c++templateslanguage-lawyervariant

Is required for class templates to be instantiated to use only pointer or reference to them?


When using just pointers/references to std::variant<T...> defined from T... where some of T is only forward declared - I have this problem that I cannot even use pointers nor references to this std::variant - because its bases classes wanted to be instantiated - and they require std::is_default_constructible and other traits.

See example code:

#include <variant>

struct A;
struct B;
using AB = std::variant<A,B>;
AB* ab();

template <typename T>
void usePointer(T*){}


int main() {
    // fails to compile because std::variant gets instantiated here!
    usePointer(ab());
}

To simplify the example - this is what happens as I see:

struct A;
struct B;

template <typename T>
void usePointer(T*){}


template <bool> class SomeBase {};

template <typename T>
struct Some : SomeBase<std::is_default_constructible_v<T>>
{};

Some<A>* someA(); 

int main() {
    // this will not compile - because  this
    // SomeBase<std::is_default_constructible_v<T>>
    // does not compile - because A is unknown
    usePointer(someA());
}
/opt/compiler-explorer/gcc-14.1.0/include/c++/14.1.0/type_traits: In instantiation of 'constexpr const bool std::is_default_constructible_v<A>':
<source>:12:29:   required from 'struct Some<A>'
   12 | struct Some : SomeBase<std::is_default_constructible_v<T>>
      |                        ~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:21:23:   required from here
   21 |     usePointer(someA());
      |                       ^
/opt/compiler-explorer/gcc-14.1.0/include/c++/14.1.0/type_traits:3385:54: error: invalid use of incomplete type 'struct A'
 3385 |   inline constexpr bool is_default_constructible_v = __is_constructible(_Tp);
      |                                                      ^~~~~~~~~~~~~~~~~~~~~~~
<source>:3:8: note: forward declaration of 'struct A'
    3 | struct A;
      |        ^
Compiler returned: 1

All 3 compilers (gcc,clang,msvc) behave/reject same way - see: https://godbolt.org/z/5cjf1Pv4d

The question: is this correct? For me this is really a bug - either in the compilers implementations or in the standard - however I cannot find anything in the standard mandating this behavior.

"Funny" thing is that when the class template is also forward declared - everything works. I mean - this code is accepted by all compilers:

struct A;

template <typename T>
void usePointer(T*){}

template <typename T> struct Some;

Some<A>* someA(); 

int main() {
    usePointer(someA());
}


Solution

  • This is because std::variant needs to be instantiated for ADL: There could be a friend void usePointer(std::variant<A, B>*) declared inside std::variant that it would have to use instead, so the template needs to be instantiated (causing it to error because std::variant needs its types to be complete when it is instantiated).

    The fix is to not use ADL:

    template <typename T>
    void usePointer(T*){}
    
    namespace ns {
    template <typename T>
    void usePointer(T*){}
    }
    
    int main() {
        // Can't be "unqualified-id ( expression-list[opt] )"
        (usePointer)(ab());
        ::usePointer(ab());
        ns::usePointer(ab());
    }
    

    https://eel.is/c++draft/basic.lookup.argdep#1

    When the postfix-expression in a function call is an unqualified-id, and unqualified lookup for the name in the unqualified-id does not find any

    • declaration of a class member, or
    • function declaration inhabiting a block scope, or
    • declaration not of a function or function template

    then lookup for the name also includes the result of argument-dependent lookup in a set of associated namespaces that depends on the types of the arguments (and for template template arguments, the namespace of the template argument), as specified below.

    usePointer does not find a class member, block scope function, or non-function/function template.

    https://eel.is/c++draft/basic.lookup.argdep#3.4

    If T is a pointer to U or an array of U, its associated entities are those associated with U.

    So T = std::variant<A, B>*, U = std::variant<A, B>

    https://eel.is/c++draft/basic.lookup.argdep#3.2.sentence-1

    If T is a class type (including unions), its associated entities are: the class itself; the class of which it is a member, if any; and, if it is a complete type, its direct and indirect base classes.

    (Prior to CWG2857, this technically didn't have to instantiate the class. But that is not the expected behaviour because then no friend functions in templates could be found before they are implicitly instantiated elsewhere, and this was a standard defect)

    So the class needs to be a complete type to look at its bases.

    https://eel.is/c++draft/temp.inst#2

    Unless a class template specialization is a declared specialization, the class template specialization is implicitly instantiated when the specialization is referenced in a context that requires a completely-defined object type or when the completeness of the class type affects the semantics of the program.

    So we have to instantiate it.

    This is also why your example of struct A; does compile: It's not a template so doesn't need to be instantiated.

    If you made it so the completeness affects the program:

    struct A;
    
    void f(auto*) { std::puts("default"); }
    void g(auto* p) { f(p); }
    int main() {
        g(static_cast<A*>(nullptr));
    }
    struct A {
        friend void f(A*) { std::puts("friend"); }
    };
    

    ... your program would become ill-formed NDR (https://eel.is/c++draft/temp.dep.res#temp.point-7.sentence-4)