Does this constexpr virtual function technique violate any C++11/C++14 rule?
Asked Answered
L

1

8

I was reading the C++ docs the other day and noticed that, although literal types must not have virtual members, this does not prevent them from implementing virtual members. Or at least that's what I understood.

This is some piece of code I've been playing with:

#include <cassert>

// Some forward declarations:

enum class literal_id;
struct literal_base;
struct literal_a;
struct literal_b;

// Now some definitions:

enum class literal_id {
    a, b 
};

struct literal_base {
    virtual literal_id method() const noexcept = 0;
};

struct literal_a : public literal_base {
    constexpr literal_id method() const noexcept final { return literal_id::a; }
    constexpr operator literal_b() const noexcept;
};

struct literal_b : public literal_base {
    constexpr literal_id method() const noexcept final { return literal_id::b; }
    constexpr operator literal_a() const noexcept;
};

constexpr literal_a::operator literal_b() const noexcept { return literal_b(); }
constexpr literal_b::operator literal_a() const noexcept { return literal_a(); }

// Some test methods

literal_id process_literal_base(literal_base const& l) { return l.method(); }
constexpr literal_id process_literal_a(literal_a const& l) { return l.method(); }
constexpr literal_id process_literal_b(literal_b const& l) { return l.method(); }

// Some test variables

constexpr auto a = literal_a();
constexpr auto b = literal_b();

int main() {
    // Compile-time tests, all ok
    static_assert(process_literal_a(b) == literal_id::a, "");
    static_assert(process_literal_b(a) == literal_id::b, "");

    // Runtime tests, all ok
    assert(process_literal_base(a) == literal_id::a);
    assert(process_literal_base(b) == literal_id::b);

    return 0;
}

Some remarks:

  • I have a base class literal_base with an implicit (and therefore trivial) destructor, because none of its subclasses are supposed to have anything but a trivial destructor -- they are literal types, after all.
  • literal_base has a single method function used for testing, but the intention is for it to have as many pure virtual functions as needed (non-virtual final functions are also valid).
  • Note that the method overrides in the subclasses are marked final, even though they are not marked virtual. This is just to silence the compiler, because those classes are supposed to either be leaf (in their inheritance tree) or have no overwritten functions. (All this has to do with pre-C++11 undefined behaviour semantics for final implementations of virtual functions, when the final specifier did not exist yet.)
  • The process_* functions were created to help with asserting the correctness of the implementation at compile and runtime.
  • I also played with value semantics, for no reason at all, and all was well :)

Some relevant definitions for literal types:

... possibly cv-qualified class type that has all of the following properties:

  • (1) has a trivial destructor. [[ they have (the subclasses of literal_base, I mean) ]]
  • (2) is either
    • (2.1) an aggregate type, [[ not applicable ]]
    • (2.2) a type with at least one constexpr (possibly template) constructor that is not a copy or move constructor, [[ it has, but just because none of the classes have an explicit constructor; but it's easy to achieve ]]
    • (2.3) a closure type (since C++17) [[ not applicable ]]
  • (3) for unions, at least one non-static data member is of non-volatile literal type, [[ not applicable ]]
  • (4) for non-unions, all non-static data members and base classes are of non-volatile literal types. (since C++17) [[ no volatile in the example, and neither should volatile be used in a real application; also, literal_base's subclasses are supposed to be literal types, so this rule must (and can) be applied ]]
  • (5) all non-static data members and base classes are of non-volatile literal types. [[ same as (4), basically ]]

Now some definitions for constexpr functions:

  • it must not be virtual [[ none of the subclasses have virtual functions; all of them are final, and therefore their locations are known without the need for e.g. vtables ]]
  • (...)

Am I right to assume all that? Is there anything about the specification I'm overlooking?

Loving answered 9/3, 2017 at 3:25 Comment(6)
§class.virtual/p2: If a virtual member function vf is declared in a class Base and in a class Derived, derived directly or indirectly from Base, a member function vf with the same name, parameter-type-list ([dcl.fct]), cv-qualification, and ref-qualifier (or absence of same) as Base::vf is declared, then Derived::vf is also virtual (whether or not it is so declared) and it overrides112 Base::vf.Ransom
For the future, when all of your identifiers have almost the same name, it's very difficult to parse your code.Erda
"none of the subclasses have virtual functions" They very much do have virtual functions. "all of them are final, and therefore their locations are known without the need for e.g. vtables." Virtual functions declared final are still virtual. final may allow the compiler to do some optimizations, but they don't make a function not be virtual. You can't declare a final function to be constexpr. Though the presence of virtual members doesn't prevent a type from being literal.Vapid
@nicol-bolas It seems all my assumptions were wrong, but even then, is this technique valid? As you said, literal types can have virtual members.Contaminate
@shafik-yaghmour Ok, but is it wrong to assume the compiler will optimize the calls so that there'll be no repercussions on using literal_a and literal_b in a constexpr context? As far as I can see, it's easy for an implementation to see that the inheritance tree does not collaborate with the function calls; the concrete functions are all final, and all superclass functions are pure virtual. The compiler should know, anywhere and anytime, the exact location of the implemented functions, isn't it?Contaminate
Just because literal types can have virtual member functions doesn't mean those member functions can be constexpr. They cannot. Period.Erda
E
12

The rule in [dcl.constexpr] is pretty clear:

The definition of a constexpr function shall satisfy the following requirements:
— it shall not be virtual (10.3);

literal_a::method and literal_b::method are both virtual because each they override literal_base::method, which is virtual. Hence, they cannot be constexpr. It does not matter that they are final. The program is ill-formed.

It is true that a literal type is allowed to have a virtual member function though.

Erda answered 9/3, 2017 at 5:11 Comment(1)
Not relevant to since C++20.Horticulture

© 2022 - 2024 — McMap. All rights reserved.