Lukas Barth
Lukas Barth

Reputation: 3048

Confusion (or Clang bug?) about incomplete types in std::vector

The C++20 standard states in [vector.overview]/4:

An incomplete type T may be used when instantiating vector if the allocator meets the allocator completeness requirements. T shall be complete before any member of the resulting specialization of vector is referenced.

The default allocator std::allocate does satisfy the allocator completeness requirements. The main question is what "referenced" means in this context. The code I am confused about are variants of this:

#include <vector>

class MyClass;

class MyContainer
{ 
        std::vector<MyClass>  member;
};

class MyClass {};

int main()
{}

The above code compiles fine in all sorts of compilers. It still compiles if I explicitly default the default constructor:

#include <vector>

class MyClass;

class MyContainer
{ 
        MyContainer()  = default;
        std::vector<MyClass>  member;
};

class MyClass {};

int main()
{}

However, when I instead define the default constructor to be "empty", something weird happens. This is the code (here at Compiler Explorer):

#include <vector>

class MyClass;

class MyContainer
{ 
        MyContainer() {};
        std::vector<MyClass>  member;
};

class MyClass {};

int main()
{}

With this code:

    In file included from <source>:1:
    In file included from /opt/compiler-explorer/gcc-12.2.0/lib/gcc/x86_64-linux-gnu/12.2.0/../../../../include/c++/12.2.0/vector:64:
    /opt/compiler-explorer/gcc-12.2.0/lib/gcc/x86_64-linux-gnu/12.2.0/../../../../include/c++/12.2.0/bits/stl_vector.h:367:35: error: arithmetic on a pointer to an incomplete type 'MyClass'
                                                _M_impl._M_end_of_storage - _M_impl._M_start);
                                                ~~~~~~~~~~~~~~~~~~~~~~~~~ ^
    /opt/compiler-explorer/gcc-12.2.0/lib/gcc/x86_64-linux-gnu/12.2.0/../../../../include/c++/12.2.0/bits/stl_vector.h:526:7: note: in instantiation of member function 'std::_Vector_base<MyClass, std::allocator<MyClass>>::~_Vector_base' requested here
                vector() = default;
                ^
    <source>:7:5: note: in defaulted default constructor for 'std::vector<MyClass>' first required here
            MyContainer() {};
            ^
    <source>:3:7: note: forward declaration of 'MyClass'
    class MyClass;

My first instinct, only looking at the Clang 15 error, was "clang is correct". The default constructor does (implicitly) invoke the default constructor of std::vector<MyClass>, and the standard says that you cannot reference members as long as MyClass is incomplete.

However, I'm pretty much sure that this cannot be the answer since:

So, my question is: Is this a Clang 15 bug? And if so, is (implicitly) invoking the default constructor of std::vector<MyClass> not considered a "reference" in terms of [vector.overview]/4?

I did search the LLVM bug tracker for the terms "vector" and "incomplete", but that did not turn something up, so if this is a known bug, it's not known in the context of std::vector, I guess.


Edit 1: I don't think this is a duplicate

This was closed as a duplicate of two questions, which in my opinion, is incorrect. The differences are subtle but relevant. The two questions are:

std::map::reverse_iterator doesn't work with C++20 when used with incomplete type

What C++20 change to reverse_iterator is breaking this code?

Upvotes: 8

Views: 628

Answers (1)

Brian Bi
Brian Bi

Reputation: 119457

It's true that "referenced" is not clear here. What I think this sentence probably means is that T shall be complete before you do anything that would require the definition of any member of the resulting specialization to exist.

In the second example

class MyClass;

class MyContainer
{ 
        MyContainer()  = default;
        std::vector<MyClass>  member;
};

class MyClass {};

the definition of std::vector<MyClass>'s default constructor is not needed until the compiler actually implicitly defines the default constructor of the enclosing class, MyContainer. And that doesn't happen until the first time MyContainer's default constructor is either odr-used or needed for constant evaluation, per the last sentence of [dcl.fct.def.default]/5:

A non-user-provided defaulted function (i.e. implicitly declared or explicitly defaulted in the class) that is not defined as deleted is implicitly defined when it is odr-used ([basic.def.odr]) or needed for constant evaluation ([expr.const]).

Your first example, where you didn't declare any default constructor, and your second example, where you declared it as defaulted inside the class definition, are treated similarly: in both cases the constructor is non-user-provided, so it does not get eagerly defined. In both examples, MyContainer has a non-user-provided copy constructor, move constructor, copy-assignment operator, move-assignment operator, and destructor, which likewise are not defined until their definitions are needed, so the program avoids "referencing" any members of std::vector<MyClass>.

In the third example, because the constructor is user-provided, it is defined even if it is never used, and its definition implicitly calls the default constructor of std::vector<MyClass>, i.e., the latter is "referenced".

When you break the rules, as you did in the third example, the program has undefined behaviour. I understand that it seems awfully unfriendly that most compilers you tried don't even warn you, but there is a reason why diagnostics are not required when you misuse incomplete types: it's difficult for templates to check for completeness in a way that does not cause bigger problems (though I believe there is ongoing work on this issue in Clang).

Upvotes: 4

Related Questions