Why the Initializer of Std::Function Has to Be Copyconstructible

Why the initializer of std::function has to be CopyConstructible?

std::function uses type erasure internally, so F has to be CopyConstructible even if the particular std::function object you are using is never copied.

A simplification on how type erasure works:

class Function
{
struct Concept {
virtual ~Concept() = default;
virtual Concept* clone() const = 0;
//...
}

template<typename F>
struct Model final : Concept {

explicit Model(F f) : data(std::move(f)) {}
Model* clone() const override { return new Model(*this); }
//...

F data;
};

std::unique_ptr<Concept> object;

public:
template<typename F>
explicit Function(F f) : object(new Model<F>(std::move(f))) {}

Function(Function const& that) : object(that.object->clone()) {}
//...

};

You have to be able to generate Model<F>::clone(), which forces F to be CopyConstructible.

Rationale behind making std::function require copy constructor

std::function could take a move-only callable, but only by restricting itself to being move-only always. It uses type-erasure to store the object, so its static behavior must represent the lowest-common-denominator functionality that it requires of the objects it stores.

Here's the thing though: there are a lot of places in C++ and its standard library that expect that a callable shall be copyable. And by "lot", I mean the entirety of the standard algorithms library. Even the C++20 concept indirect_unary_predicate requires copy_constructible. Essentially, all algorithms are given the freedom to copy the given function at will; they even take those functions by value.

A move-only std::function type could never be used with any such algorithm. And being move-only is the only way for std::function to take move-only types.

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.

Why can't I move a mutable function that contains a moved future?

function_which_should_be_movable is of type std::function. According to cppreference:

template< class F > function( F f ); F must meet the requirements
of Callable and CopyConstructible.

The lambda expression you try to use to construct an std::function object is not copyable, and hence the problem.

As to why std::function has this requirement, please see this question: Why the initializer of std::function has to be CopyConstructible? (which is asked exactly by myself). Simply put, the type erasure technique used by std::function will instantiate the copy-constructor of F. This happens regardless of whether you, the user of the std::function object, actually used this copy-constructor or not.

Copy Construction in Initializer Lists

The issue is that this type:

struct NonCopyable {
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete;
};

is trivially copyable. So as an optimization, since std::initializer_list is just backed by an array, what libstdc++ is doing is simply memcpying the the whole contents into the vector as an optimization. Note that this type is trivially copyable even though it has a deleted copy constructor!

This is why when you make the default constructor user-provided (by just writing ; instead of = default;), is suddenly doesn't compile anymore. That makes the type no longer trivially copyable, and hence the memcpy path goes away.

As to whether or not this behavior is correct, I am not sure (I doubt there's a requirement that this code must not compile? I submitted 89164 just in case). You certainly want libstdc++ to take that path in the case of trivially copyable - but maybe it needs to exclude this case? In any case, you can accomplish the same by additionally deleting the copy assignment operator (which you probably want to do anyway) - that would also end up with the type not being trivially copyable.

This didn't compile in C++14 because you could not construct the std::initializer_list - copy-initialization there required the copy constructor. But in C++17 with guaranteed copy elision, the construction of std::initializer_list is fine. But the problem of actually constructing the vector is totally separate from std::initializer_list (indeed, this is a total red herring). Consider:

void foo(NonCopyable const* f, NonCopyable const* l) {
std::vector<NonCopyable>(f, l);
}

That compiles in C++11 just fine... at least since gcc 4.9.

Why require copy constructor for packaged_task in VS

This is a known bug in MSVC's packaged_task implementation. They're storing the callable within std::function, which requires that the argument be copy-constructible.



Related Topics



Leave a reply



Submit