Why Does Std::Vector Work with Incomplete Types in Class Definitions

Why does std::vector work with incomplete types in class definitions?

Standard says (draft N3690; this is post C++11, pre C++14):

[res.on.functions]

1 In certain cases (replacement functions, handler functions,
operations on types used to instantiate standard library template
components), the C++standard library depends on components supplied by
a C++program. If these components do not meet their requirements,
the Standard places no requirements on the implementation.

2 In particular, the effects are undefined in the following cases:

— if an incomplete type (3.9) is used as a template argument when
instantiating a template component, unless specifically allowed for
that component.

Given that standard places no requirements, and effects are undefined (as far as I can tell, this is same as undefined behaviour), there is no expectation for the instantiation to "not work" any more than there is expectation for it to (appear to) "work".


Since C++17, the requirement was relaxed and std::vector does not require the value type to be complete, if used with appropriate allocator (the default allocator is appropriate). (This freedom does not extend to using all member functions; they have additional requirements).

Standard quote (current draft):

[vector.overview]

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.

[allocator.requirements.completeness]

If X is an allocator class for type T, X additionally meets the allocator completeness requirements if, whether or not T is a complete type:

  • X is a complete type, and
  • all the member types of allocator_­traits other than value_­type are complete types.

[default.allocator]

All specializations of the default allocator meet the allocator completeness requirements ([allocator.requirements.completeness]).

Which STL data structures with an incomplete type stored can be used as a class member?

Assuming none of the classes members are used explicitly or implicitly until the type is complete:

The template argument can always be incomplete for std::unique_ptr and std::shared_ptr since C++11, see [unique.ptr]/5 and [util.smartptr.shared]/2 respectively.

Support of incomplete types in containers was added with N4510 to C++17, but only for

std::vector
std::list
std::forward_list

and only if the allocator used fulfills the allocator completeness requirements, namely that, even if the value type itself is not complete, the allocator type X itself is a complete type and so are all members of std::allocator_traits<X>, except ::value_type. The default allocator std::allocator fulfills these requirements.

None of the other containers can be used with incomplete types. According to the proposal linked above the scope was limited to these three containers "as a first step" because the major implementations already had support for it.

Does using an incomplete type within its own definition require a diagnostic?

It depends on the exact rule. Violations of most rules related to incomplete class types in the core language section of the Standard must be diagnosed. Uses as a template argument to a Standard Library template are undefined behavior unless otherwise specified, which gives a bit more latitude to implementations.

These rules require diagnosis (since they are stated with "shall"):

[basic.def]/5:

In the definition of an object, the type of that object shall not be an incomplete type ([basic.types]), an abstract class type, or a (possibly multi-dimensional) array thereof.

[dcl.fct.def.general]/2:

The type of a parameter or the return type for a function definition shall not be a (possibly cv-qualified) class type that is incomplete or abstract within the function body unless the function is deleted ([dcl.fct.def.delete]).

[expr.ref]/4 (concerning the type of the operand expression before a . operator):

The class type shall be complete unless the class member access appears in the definition of that class. [ Note: If the class is incomplete, lookup in the complete class type is required to refer to the same declaration ([basic.scope.class]). — end note ]

Since the built-in meaning of the -> operator is defined with A->B equivalent to (*A).B, this also applies to -> operator expressions.

[class.mem]/15:

The type of a non-static data member shall not be an incomplete type ([basic.types]), an abstract class type ([class.abstract]), or a (possibly multi-dimensional) array thereof. [ Note: In particular, a class C cannot contain a non-static member of class C, but it can contain a pointer or reference to an object of class C. — end note ]

[class.derived]/2 (concerning the list of base classes for a class definition):

A class-or-decltype shall denote a (possibly cv-qualified) class type that is not an incompletely defined class ([class.mem]); any cv-qualifiers are ignored.

There are more core language rules forbidding incomplete class types, but the above are the most commonly relevant ones. See also the non-normative list of contexts requiring a complete class in [basic.def.odr]/12.

I don't see a direct rule that a qualified-id scope::name is ill-formed if scope names an incomplete class type, but it just may be that the name lookup will certainly fail in that case, which is a diagnosable violation.

For the Standard Library, the overall prohibition on incomplete types as template arguments is [res.on.functions]/(2.5):

In certain cases (replacement functions, handler functions, operations on types used to instantiate standard library template components), the C++ standard library depends on components supplied by a C++ program. If these components do not meet their requirements, this document places no requirements on the implementation.

In particular, the effects are undefined in the following cases:

  • ...

  • If an incomplete type ([basic.types]) is used as a template argument when instantiating a template component or evaluating a concept, unless specifically allowed for that component.

As noted, C++17 added specific permission to instantiate class std::vector<T, Alloc>, but none of its members, if T is incomplete and Alloc satisfies the "allocator completeness requirements" ([vector.overview]/4)

Incomplete type for std::vector

One way to fix this is to declare the destructors and constructors of Element and Face but not define them in the header.
Then you need to define them in cpp files.

(More technical details can be found in the answers to my question there: Is std::unique_ptr<T> required to know the full definition of T?)

The source of the problem is that the destructor of unique_ptr needs to call delete (by default) therefore it needs to know the definition of the type (to have it's size). But if the destructor of Element and Face is generated automatically, then it will be inlined by default: the code using Element and Face instances will be forced to know the size of both types so that their destructors can call unique_ptr destructor which can call delete with the type associated to the pointer.

The solution I gave will make sure the construction and destruction of the unique_ptr are defiend in a separate cpp. They will not be inlined but they are still callable by the code using Element and Face. The destructor code of the unique_ptrs will be in the cpp where the destructors of Element and Face are defined, so in these cpp the definition of both will be needed.

To take your example:

//header
class Face; // needs to be incomplete

class Element
{
public:
Element(); // don't define it here
~Element(); // don't define it here
private:
std::vector < std::unique_ptr <Face> > face;
};

class Face
{
public:
Face(); // don't define it here
~Face(); // don't define it here
private:
std::vector < std::unique_ptr <Element> > elm;
};

// cpp
#include "header"
// if you want the default impl (C++11)
Element::Element() = default;
Element::~Element() = default;

Face::Face() = default;
Face::~Face() = default;

In case they are in different header/cpp pair, it's still the same solution. However you have to do more forward declare and the cpp files defining the construction/destruction have to include all the necessary headers:

//element.h
class Face; // needs to be incomplete

class Element
{
public:
Element(); // don't define it here
~Element(); // don't define it here
private:
std::vector < std::unique_ptr <Face> > face;
};

////////////////////////////////////////////////////////////
// face.h
class Element; // needs to be incomplete

class Face
{
public:
Face(); // don't define it here
~Face(); // don't define it here
private:
std::vector < std::unique_ptr <Element> > elm;
};

////////////////////////////////////////////////////////////
// element.cpp
#include "element.h"
#include "face.h" // necessary to allow the unique_ptr destructor to call delete

// if you want the default impl (C++11)
Element::Element() = default;
Element::~Element() = default;

////////////////////////////////////////////////////////////
// face.cpp
#include "element.h" // necessary to allow the unique_ptr destructor to call delete
#include "face.h"

// if you want the default impl (C++11)
Face::Face() = default;
Face::~Face() = default;

Is a vector with incomplete type allowed if absolutely no member functions are called? If so, since when?

Instantiating std::vector<T> with incomplete type T is undefined behavior up to C++14. In C++17 this limitation is relaxed somewhat:

[vector.overview]/3 An incomplete type T may be used when instantiating vector if the allocator satisfies the allocator completeness
requirements 17.6.3.5.1. T shall be complete before any member of the resulting specialization of vector is referenced.

(Note: the default allocator std::allocator does satisfy those completeness requirements).

My reading is that with C++17, it's legal for a translation unit to include your header (the one that forward-declares Hidden and defines Public), and define a variable Public pub; - but not to actually use any members of pub.d.v. Before C++17, merely including the header would already trigger undefined behavior.

Is a std::function member variable of the class type allowed (incomplete type)?

It is true that, in general, it is invalid to instantiate templates from the standard library with arguments that are incomplete types:

[res.on.functions]/2 In particular, the effects are undefined in the following cases:

(2.5) — if an incomplete type (6.9) is used as a template argument when instantiating a template component, unless specifically allowed for that component.

However, Foo(Foo& f) is not an incomplete type, even if Foo is:

[basic.types]/5 A class that has been declared but not defined, an enumeration type in certain contexts (10.2), or an array of unknown bound or of incomplete element type, is an incompletely-defined object type. Incompletely-defined object types and cv void are incomplete types...

Foo(Foo& f) is a function type. It's not listed in this definition, so cannot possibly be incomplete.


std::vector is specifically allowed to be instantiated with an argument that is an incomplete type:

[vector.overview]/3 An incomplete type T may be used when instantiating vector if the allocator satisfies the allocator completeness requirements (20.5.3.5.1). T shall be complete before any member of the resulting specialization of vector is referenced.

Why is std::is_copy_constructible_vstd::vectorMoveOnlyType true?

std::vector and other containers (except std::array) are specified to have a copy constructor. This is not specified to be conditional on whether or not the element type is copyable. Only instantiation of the copy constructor's definition is forbidden if the element type is not copyable.

As a result std::is_copy_constructible_v on the container will always be true. There is no way to test whether an instantiation of a definition would be well-formed with a type trait.

It would be possible to specify that the copy constructor is not declared or excluded from overload resolution if the element type is not copyable. However, that would come with a trade-off which is explained in detail in this blog post: https://quuxplusone.github.io/blog/2020/02/05/vector-is-copyable-except-when-its-not/.

In short, if we want to be able to use the container with an incomplete type, e.g. recursively like

struct X {
std::vector<X> x;
};

then we cannot determine whether X is copyable when the container class is instantiated. Therefore the declaration of the copy constructor cannot be made dependent on this property.

Since C++17 the standard requires std::vector, std::list and std::forward_list, but not the other containers, to work like this with incomplete types.



Related Topics



Leave a reply



Submit