Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c++

stdtuple

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!

like image 466
Lyve Avatar asked Oct 30 '25 03:10

Lyve


1 Answers

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;
};
like image 63
Jan Schultke Avatar answered Nov 01 '25 16:11

Jan Schultke