Reputation: 5095
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_assert
s compile)
Now I have three questions (sorry if they are a bit unrelated):
!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.
Upvotes: 5
Views: 379
Reputation: 7488
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
.
Upvotes: 2