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 singlemethod
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 markedfinal
, even though they are not markedvirtual
. 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 thefinal
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 shouldvolatile
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?
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.
– Ransomfinal
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 afinal
function to beconstexpr
. Though the presence of virtual members doesn't prevent a type from being literal. – Vapidconstexpr
. They cannot. Period. – Erda