Initializer_List and Move Semantics

initializer_list and move semantics

No, that won't work as intended; you will still get copies. I'm pretty surprised by this, as I'd thought that initializer_list existed to keep an array of temporaries until they were move'd.

begin and end for initializer_list return const T *, so the result of move in your code is T const && — an immutable rvalue reference. Such an expression can't meaningfully be moved from. It will bind to an function parameter of type T const & because rvalues do bind to const lvalue references, and you will still see copy semantics.

Probably the reason for this is so the compiler can elect to make the initializer_list a statically-initialized constant, but it seems it would be cleaner to make its type initializer_list or const initializer_list at the compiler's discretion, so the user doesn't know whether to expect a const or mutable result from begin and end. But that's just my gut feeling, probably there's a good reason I'm wrong.

Update: I've written an ISO proposal for initializer_list support of move-only types. It's only a first draft, and it's not implemented anywhere yet, but you can see it for more analysis of the problem.

how to move elements of an initializer_list?

There is no way to avoid the copying from an initializer_list<string>, because the standard defines the invocation of a constructor taking an initializer list argument, from a curly braces initializer as actual argument, as follows (emphasis added):

C++14 §8.5.4/5

An object of type std::initializer_list<E> is constructed from an initializer list as if the implementation allocated a temporary array of N elements of type const E, where N is the number of elements in the
initializer list

IMHO this is really unfortunate.

A workaround (for your own classes) is to accept initializer_list<char const*>.


Here's an example of the workaround applied to std::vector<string>. For that, where you don't control the class' code, it involves declaring a data array (actually an initializer_list) explicitly. This is just as with C++03, which the initializer list mechanism was intended to avoid:

#include <vector>
#include <initializer_list>
#include <iostream>
#include <iterator> // std::begin, std::end
using namespace std;

struct My_string
{
char const* const ps;

My_string( char const* const s )
: ps( s )
{
cout << " My_string(*) <- '" << s << "'" << endl;
}

My_string( My_string const& other )
: ps( other.ps )
{
cout << " My_string(const&) <- '" << other.ps << "'" << endl;
};

My_string( My_string&& other )
: ps( other.ps )
{
cout << " My_string(&&) <- '" << other.ps << "'" << endl;
};
};

auto main() -> int
{
cout << "Making vector a." << endl;
vector<My_string> const a = {"a1", "a2", "a3"};
cout << "Making data for vector b." << endl;
auto const b_data = { "b1", "b2", "b3" };
cout << "Making vector b." << endl;
vector<My_string> const b( begin( b_data ), end( b_data ) );
}

Output:


Making vector a.
My_string(*) <- 'a1'
My_string(*) <- 'a2'
My_string(*) <- 'a3'
My_string(const&) <- 'a1'
My_string(const&) <- 'a2'
My_string(const&) <- 'a3'
Making data for vector b.
Making vector b.
My_string(*) <- 'b1'
My_string(*) <- 'b2'
My_string(*) <- 'b3'

Why isn't move construction used when initiating a vector from initializer list (via implicit constructor)

I thought I had an initializer list of integers, but I'm now wondering if what I have in between is an initializer list of Cs, which can't be moved from (as its const). Is this a correct interpretation?

This is correct. vector<C> does not have an initializer_list<int> constructor or even an initializer_list<T> constructor for some template parameter T. What it does have is an initializer_list<C> constructor - which is built up from all the ints you pass in. Since the backing of initializer_list is a const array, you get a bunch of copies instead of a bunch of moves.

Is it safe to move elements of a initializer list?

initializer_list only provides const access to its elements. You could use const_cast to make that code compile, but then the moves might end up with undefined behaviour (if the elements of the initializer_list are truly const). So, no it is not safe to do this moving. There are workarounds for this, if you truly need it.

using an initializer list where the values contain unique_ptr member variables

It's fault of std::initializer_list, if you look at it's begin/end member functions, they return const T*, which means it will force std::map to try use the copy constructor of your Foo, which is deleted as std::unique_ptr can not be copied.

This issue is not unique to std::map, any container which allows you to initialize it with std::initializer_list will really copy the arguments from an initializer list.

The C++ standard requires that during such initialization there's a temporary const T[N] array, which the std::initializer_list points to, const disables moves from it.

Why are copy and move constructors called together?

std::vector can be constructed from a std::initializer_list, and you are calling that constructor. The rules for initializer_list construction state that this constructor is aggressively preferred:

A constructor is an initializer-list constructor if its first parameter is of type std::initializer_list<E>
or reference to possibly cv-qualified std::initializer_list<E> for some type E, and either there are
no other parameters or else all other parameters have default arguments (8.3.6). [ Note: Initializer-list
constructors are favored over other constructors in list-initialization <...>]

Also, because of the sort of weird implementation of an initializer_list as an array allocated under the hood, elements of the corresponding array that the std::initializer_list<E> refers to are forced to be copy initialized (which can be elided):

An object of type std::initializer_list<E> is constructed from an initializer list as if the implementation
allocated an array of N elements of type E, where N is the number of elements in the initializer list.
Each element of that array is copy-initialized with the corresponding element of the initializer list, and
the std::initializer_list<E> object is constructed to refer to that array

(Both references above from N3337 [dcl.init.list])

However, in your first example the copies can/are elided despite the name ([dcl.init]/14) so you don't see an extra copy construction (they can also be moved) You can thank your compiler for that, because copy elision is not required in C++11 (although it is in C++17).

See [class.copy] for more details ("When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class
object...").

The final part is key:

[support.initlist] states that

An object of type initializer_list<E> provides access to an array of objects of type const E.

This means that the std::vector cannot take over the memory directly; it must be copied, this is where you ultimately see the copy constructions being called.

In the second example it is as Kerrek SB stated, you prevented the copy-elision I mentioned earlier and caused an additional overhead of a move.



Related Topics



Leave a reply



Submit