c++stdtuple

std::tuple missing default constructor when using an inner struct


The following code compiles fine on msvc and clang 17+ but does not on compile on gcc 14 or clang <= 16. However, it compiles as soon as I move the inner struct out to global scope.

Edit: I just noticed that another fix is to remove the explicit default constructor of the template.

#include <tuple>

template<typename ...Ts> struct myTemplate
{
    using MyTuple = std::tuple<Ts...>;
    
    myTemplate() = default;

    MyTuple tupleMember;
};

struct myOuterStruct
{
    struct myInnerStruct
    {
        int someMember=0;
    };

    myTemplate<myInnerStruct> someMember;
};


myOuterStruct someVariable;

The error is that there is no default constructor for the tuple, even though the inner struct is default constructible indeed.

<source>:23:15: error: call to implicitly-deleted default constructor of 'myOuterStruct'
myOuterStruct someVariable;
              ^
<source>:19:31: note: default constructor of 'myOuterStruct' is implicitly deleted because field 'someMember' has a deleted default constructor
    myTemplate<myInnerStruct> someMember;
                              ^
<source>:7:5: note: explicitly defaulted function was implicitly deleted here
    myTemplate() = default;
    ^
<source>:9:13: note: default constructor of 'myTemplate<myOuterStruct::myInnerStruct>' is implicitly deleted because field 'tupleMember' has no default constructor
    MyTuple tupleMember;

Anyone could shed some light into this?

I prepared it in godbolt here: https://godbolt.org/z/b5b4Y43q7

Thanks in advance!


Solution

  • Your code is ill-formed, but this is arguably a defect in the standard.

    Consider [class.mem.general] p18:

    The type of a non-static data member shall not be an incomplete type [...]

    This means that myTemplate<myInnerStruct> someMember; causes an instantiation of myTemplate<myInnerStruct> as per [temp.inst] p2. The point of instantiation precedes myOuterStruct ([temp.point] p2), so it is as if you wrote:

    // pseudo-code
    template struct myTemplate<myOuterStruct::myInnerStruct>
    {
        using MyTuple = std::tuple<myOuterStruct::myInnerStruct>;
        
        myTemplate() = default;
    
        MyTuple tupleMember;
    };
    
    struct myOuterStruct
    {
        // ...
    };
    

    At this point, MyTuple is a complete type, and one is required, so std::tuple<myInnerStruct> is instantiated while myInnerStruct is still incomplete. This leads to further problems down the line; namely, the tuple isn't default constructible (and presumably broken in other ways).

    Note that some compilers (e.g. clang trunk) correctly allow this code because there is also possible single point of instantiation for myTemplate at the end of the translation unit ([temp.point] p7). However, this should be seen as a fluke; it's not reliable.

    Related defect

    CWG Issue 1890 is seemingly related to this, and provides the example (with a surprisingly failing assertion):

    #include <type_traits>
    
    struct Bar {
      struct Baz {
        int a = 0;
      };
      static_assert(std::is_default_constructible_v<Baz>);
    };
    

    As with your code, the issue is that std::is_default_constructible is instantiated at a point before Bar, and Baz is incomplete at that point.

    Workaround

    Avoid the use of a nested class:

    struct myInnerStructImpl
    {
        int someMember=0;
    };
    
    // Point of instantiation of myTemplate<myInnerStruct> is here.
    // myInnerStructImpl is complete.
    
    struct myOuterStruct
    {
        using myInnerStruct = myInnerStructImpl;
    
        myTemplate<myInnerStruct> someMember;
    };