Following code fails to be compiled with some compilers (including VS v19.20)
#include <variant>
struct A {
int a;
bool b;
using T1 = int A::*;
using T2 = bool A::*;
using U = std::variant<T1,T2>;
static constexpr const U Ptrs[] = {
{&A::a}, {&A::b}
};
};
VS output:
<source>(23): error C2327: 'A::a': is not a type name, static, or enumerator
<source>(23): error C2065: 'a': undeclared identifier
<source>(23): error C2327: 'A::b': is not a type name, static, or enumerator
<source>(23): error C2065: 'b': undeclared identifier
<source>(24): fatal error C1903: unable to recover from previous error(s); stopping compilation
Similar output happens in case of using a custom union type nested within A
struct A {
int a;
bool b;
using T1 = int A::*;
using T2 = bool A::*;
template <class T1, class T2>
union MyUn {
T1 pA;
T2 pB;
constexpr MyUn(T1 a) : pA(a) {}
constexpr MyUn(T2 b) : pB(b) {}
};
using U = MyUn<T1,T2>;
static constexpr const U Ptrs[] = {
{&A::a}, {&A::b}
};
};
which appears to be legit and supported by GCC:
<source>:22:5: error: 'constexpr A::MyUn<T1, T2>::MyUn(T1) [with T1 = int A::*; T2 = bool A::*]' used before its definition
Moving Ptrs
to location where A
is complete works in both cases, for all compilers, which leads to idea that in first case the template std::variant
type is instanced as A::U
in scope of class A
:
struct B : A {
static constexpr const U Ptrs[] = {
{&A::a}, {&A::b}
};
};
But GCC 15.1 rejects following code for any types T1 and T2:
struct B : A {
template <class T1, class T2>
union MyUn {
T1 pA;
T2 pB;
constexpr MyUn(T1 a) : pA(a) {}
constexpr MyUn(T2 b) : pB(b) {}
};
using U = MyUn<T1,T2>;
static constexpr const U Ptrs[] = {
{T1()}, {T2()}
};
};
Is first case an allowed behavior and I'm doing something undefined\implementation-defined or it's a genuine compiler bug shared between some versions and vendors?
Pointer to members in the class declaration is a legit part of constant expression and should be allowed. It's MSVC bug which was fixed as part of 17.11 VS release (MSVC 19.41).
Whether the std::variant
initalization is a constant expression or not is not under question, so I'll skip this part. The error MSVC is complaining about is whether class members can be accessed in context of initializing its own static constexpr
member variable. The question is quite speculative, as per this conclusion in another SO discussion the class is incomplete in this context (credits to OP for pointing this out in the comment discussion). By definition given in class.mem#6 I tend to agree with it (emphasis belongs to AngyG):
A class is considered a completely-defined object type ([basic.types]) (or complete type) at the closing
}
of the class-specifier. Within the class member-specification, the class is regarded as complete within function bodies, default arguments, noexcept-specifiers, and default member initializers (including such things in nested classes). Otherwise it is regarded as incomplete within its own class member-specification.
Thus, the question boils to down to whether it's allowed to refer to data members of incomplete class or not.
expr.prim.id.qual/2 says as follows (emphasis mine):
A nested-name-specifier that denotes a class, optionally followed by the keyword template ([temp.names]), and then followed by the name of a member of either that class ([class.mem]) or one of its base classes, is a qualified-id; [class.qual] describes name lookup for class members that appear in qualified-ids. The result is the member. The type of the result is the type of the member. The result is an lvalue if the member is a static member function or a data member and a prvalue otherwise. [ Note: A class member can be referred to using a qualified-id at any point in its potential scope ([basic.scope.class]). — end note ]
In basic.scope.class/1 the potential scope is described as follows (emphasis mine):
The potential scope of a name declared in a class consists not only of the declarative region following the name's point of declaration, but also of all function bodies, default arguments, noexcept-specifiers, and brace-or-equal-initializers of non-static data members in that class (including such things in nested classes)
The basic.scope.pdecl/6 extends it a little further (emphasis mine):
After the point of declaration of a class member, the member name can be looked up in the scope of its class. [ Note: This is true even if the class is an incomplete class. For example,
struct X { enum E { z = 16 }; int b[X::z]; // OK };
— end note ]