On Sun, 2 Oct 2022 at 22:00, Aleksander Maciej Miera via Std-Discussion <std-discussion@lists.isocpp.org> wrote:
Hello,

While fiddling with template metaprogramming I managed to run into an
issue related to some std:: containers and type traits. As far as I have
researched it, it is a known (although not obvious at first) shortcoming.

Consider the following code snippet:

#include <type_traits>
#include <memory>
#include <vector>

struct Copyable{};
using MoveOnly = std::unique_ptr<Copyable>;

static_assert(!std::is_copy_constructible_v<std::vector<MoveOnly>>);

(BTW, the vector is used here as an example, but the list and
forward_list also behave the same way for the same reason)

Counter-intuitively, the static assert fails. This stems from the fact
that the vector's copy constructor is not a function template; thus it
cannot be SFINAEd (everything can be a verb if one tries hard enough ;))
away, because it's always declared.

Since C++20, the copy constructor can be constrained:

    vector(vector const&) requires std::copyable<T>;

I think this would solve your issue. This would require the containers general requirements to lift this requirement (on `X u(a)`, that `T` is Cpp17CopyInsertable, etc.) from a precondition to a constraint. There are probably quite a few clauses that would need to be considered in such an effort.

It is logical in terms of how the language works, but at the same time
it is incorrect in terms of API's predictability.

I've been thinking if it can be fixed in some way. For C++11 and 14 I
believe that making the vector's copy constructor a function template
restricted by value_type being copyable should do.
However, that ship seems to have sailed in C++17 and further versions,
as vector is required by the standard to support incomplete types.

I wonder if this issue can be addressed by specialising
std::is_copy_constructible, std::is_copy_assignable and similar traits
based on vector's value_type, e.g.:

template <typename>
struct is_vector_copy_constructible;

template <typename T>
struct is_vector_copy_constructible<std::vector<T>>
     : std::conditional_t<std::is_copy_constructible_v<T>,
std::true_type, std::false_type>
{};

template<typename T>
inline bool constexpr is_vector_copy_constructible_v =
is_vector_copy_constructible<T>::value;

static_assert(!is_vector_copy_constructible_v<std::vector<MoveOnly>>);
static_assert(is_vector_copy_constructible_v<std::vector<Copyable>>);

(for the sake of simplicity all allocator-related subtleties are omitted)

The obvious result would be the change of behaviour for incomplete
types. Therefore code like this:

struct Incomplete;
static_assert(!is_vector_copy_constructible_v<std::vector<Incomplete>>
|| is_vector_copy_constructible_v<std::vector<Incomplete>>);

would fail to compile. It seems a reasonable way of the compiler
communicating "I have no idea", but it would be a breaking change should
is_vector_copy_constructible become a partial specialisation of
is_copy_constructible.

The question is: is it even the right direction I'm headed with my
question? If so, is there a better way to address this or maybe it is a
"broken by design" case and I shouldn't even bother?

In case they're needed, live demos of my code snippets are here:
https://godbolt.org/z/za8q63njE

Best regards,
A. M. Miera
--
Std-Discussion mailing list
Std-Discussion@lists.isocpp.org
https://lists.isocpp.org/mailman/listinfo.cgi/std-discussion