Search code examples
c++templatescopy-constructorsfinaeincomplete-type

std::is_copy_constructable for std::vector


I recently read this blogpost on why vector has to be unconditionally copyable so it can support incomplete types. I understand that this is necessary also from a logical point of view, since the following has a circular dependency on copyability:

struct Test {
    std::vector<Test> v;
};

Now I thought about, whether or not one could at least try to give the best information available. In other words, std::vector<T> is copy constructable if and only if T is copy constructable or incomplete. So std::vector<std::unique_ptr<T>> would never be copy constructible since std::unique_vector is move-only, independent of T.

I came to the following solution:

#include <type_traits>
#include <memory>


template<class T, class = decltype(sizeof(int))>
struct is_complete : std::false_type {};

template<class T>
struct is_complete<T, decltype(sizeof(T))> : std::true_type{};

template<class T>
constexpr bool is_complete_v = is_complete<T>::value;

// Indirection to avoid instantiation of is_copy_constructible with incomplete type
template<class T, class = std::enable_if_t<is_complete_v<T>>>
struct copyable {
    static constexpr bool value = std::is_copy_constructible_v<T>;
};

template<class T>
struct copyable<T, void> : std::true_type {};

template<class T>
struct Container {

    template<class T1 = T, class = std::enable_if_t<copyable<T1>::value>>
    Container(const Container &) {}
};

struct A;
struct B{};

static_assert(!is_complete_v<A>);
static_assert(is_complete_v<B>);
static_assert(std::is_copy_constructible_v<Container<A>>);
static_assert(std::is_copy_constructible_v<Container<B>>);
static_assert(!std::is_copy_constructible_v<std::unique_ptr<A>>);
static_assert(!std::is_copy_constructible_v<std::unique_ptr<B>>);

struct A{};

static_assert(!is_complete_v<A>);

godbolt (All static_asserts compile)

Now I have three questions (sorry if they are a bit unrelated):

  1. Is this code valid standard C++ or does it rely on undefined behavior anywhere?
  2. What do you think about this idea?
  3. I first had for the copy constructor the SFINAE condition !is_complete_v<T1> || std::is_copy_constructible_v<T1> but I had to add indirection because otherwise clang (not gcc) would not compile due to std::is_copy_constructible being instantiated with an incomplete type. Does || not also short-circuit the instantiation of templates?

Regarding 1., in my opinion there should be no UB. The one part where it could happen is sizeof(T), since one should not use that with an incomplete type. But SFINAE-ing with sizeof has a long tradition from when it was the only unevaluated context, so I would think that is ok.

Regarding 2., I know that this makes whether or not a vector<T> is copy constructible very fragile, since if one adds a forward declaration of an otherwise complete T somewhere at an unrelated part of the code and then also checks it completeness, this will change the completeness of T for the whole project. I am not sure if the small increase in available information is worth this.


Solution

  • necessary also from a logical point of view, since the following has a circular dependency on copyability:

    struct Test {
        std::vector<Test> v;
    };
    

    That doesn't make it logically necessary. Function a can call function b which calls function a. It is necessary given the premise that you have to answer the question when the declaration of v is encountered inside the declaration of Test. In current C++ as we know it, it is necessary, but that follows from the various rules we ourselves impose.

    Is this code valid standard C++ or does it rely on undefined behavior anywhere?

    UB. Template specializations cannot have different meaning at different points of instantiation. Specifically, a "... static data member of a class template may have multiple points of instantiations within a translation unit" including always the end temp.point/7. The compiler is free to instantiate is_complete<T>::value at the end of the translation unit, in addition to the other places. The program is ill-formed if this gives a different answer at different instantiation points.

    So you cannot instantiate is_complete with a type which is incomplete but will later be complete, like Test.