18

Let's say you have a variable of type std::vector<std::string> and you initialize it with an initializer list:

using V = std::vector<std::string>;
V v = { "Hello", "little", "world", "of", "move", "semantics" };

The compiler will create a temporary std::string for each string literal, create an initializer list over these and then call the ctor for V and create the vector. The ctor does not know that all those strings are temporaries, so it is copying each string.

I haven't found anything in the standard which allows the vector ctor to move the elements when they are temporaries.

Am I missing something or does using initializer lists lead to unnecessary copies? I am writing classes where this problem could lead to significantly inefficient code. Any technique to avoid unnecessary copies would be greatly appreciated.

Daniel Frey
  • 55,810
  • 13
  • 122
  • 180
  • Good question. I don't think initializer lists were originally *intended* to do anything but pass a sequence of temporaries around. But now you can declare an initializer list variable. It wouldn't do for a vector to suck the innards out of each item in such a long lived list. But wait, constructor with rvalue ref to initializer list. Maybe? – Cheers and hth. - Alf Apr 02 '16 at 19:30
  • @Cheersandhth.-Alf I guess I can overload on an initializer list by const-ref, non-const-ref and rvalue-ref. I can then move the elements from the rvalue-ref initializer list manually. Not elegant and doesn't work with the STL containers I'm currently using as underlying storage (vector and map) but better than nothing. Thanks. – Daniel Frey Apr 02 '16 at 19:45
  • duplicate of [initializer\_list and move semantics](https://stackoverflow.com/questions/8193102/initializer-list-and-move-semantics) – underscore_d Jul 05 '17 at 09:16

3 Answers3

7

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'
Cheers and hth. - Alf
  • 142,714
  • 15
  • 209
  • 331
  • You can also use a variadic template constructor. – o11c Apr 02 '16 at 19:56
  • `const E`? Thank is *really* unfortunate. Is there any reason for this? Otherwise, detecting an rvalue initializer list would at least allow me to move manually, but with `const E`, that seems illegal!? (And I am not really using `std::string`, I'm using a type I wrote that can be more expensive to copy) – Daniel Frey Apr 02 '16 at 20:15
  • I think I found a solution by using `mutable`, I added a self-answer. What do you think about it? – Daniel Frey Apr 04 '16 at 19:17
  • Neat. I didn't think of that. I can't see that it helps much with `vector`, but should great for user-defined classes. I think the proxy thing can be generalized to a general `mutable_` class template. I.e. writing `mutable_` instead of `my_string_proxy`. – Cheers and hth. - Alf Apr 04 '16 at 19:33
  • In case you are interested: Here's the real-world use-case I have... https://github.com/taocpp/json – Daniel Frey Apr 04 '16 at 21:05
4

After some thinking, I came up with a solution based on mutable. The other answer is still mostly correct, but one can create a proxy with a mutable member to get rid of the top-level const-ness and then move the elements from there. Methods taking an initializer list should therefore overload for a const-ref initializer list and an rvalue-ref version to know when they are allowed to move.

Here's a working example, it might look arbitrary at first but in my real-world use-case, it solved the problem.

#include <iostream>
#include <vector>

// to show which operations are called
struct my_string
{
    const char* s_;
    my_string( const char* s ) : s_( s ) { std::cout << "my_string(const char*) " << s_ << std::endl; }
    my_string( const my_string& m ) : s_( m.s_ ) { std::cout << "my_string(const my_string&) " << s_ << std::endl; }
    my_string( my_string&& m ) noexcept : s_( m.s_ ) { std::cout << "my_string(my_string&&) " << s_ << std::endl; }
    ~my_string() { std::cout << "~my_string() " << s_ << std::endl; }
};

// the proxy
struct my_string_proxy
{
    mutable my_string s_;

    // add all ctors needed to initialize my_string
    my_string_proxy( const char* s ) : s_( s ) {}
};

// functions/methods should be overloaded
// for the initializer list versions

void insert( std::vector<my_string>& v, const std::initializer_list<my_string_proxy>& il )
{
    for( auto& e : il ) {
        v.push_back( e.s_ );
    }
}

void insert( std::vector<my_string>& v, std::initializer_list<my_string_proxy>&& il )
{
    for( auto& e : il ) {
        v.push_back( std::move( e.s_ ) );
    }
}

int main()
{
    std::vector<my_string> words;
    insert( words, { {"Hello"}, {"initializer"}, {"with"}, {"move"}, {"support"} } );
}

Live example

Daniel Frey
  • 55,810
  • 13
  • 122
  • 180
0

Instead of taking an initializer list, for the cost of a template, you can take an rvalue reference of array:

template<typename T, std::size_t n>
void insert(std::vector<T>& vector, T(&&elements)[n])
{
    for (T& element : elements)
        vector.push_back(std::move(element));
}

(The template parameter T is not necessary for the technique if you know the type, but n is.)

int main()
{
    std::vector<std::unique_ptr<std::string>> strings;
    // C++14 onward: use std::make_unique
    insert(strings, {
        std::unique_ptr<std::string>{ new std::string{} },
        std::unique_ptr<std::string>{ new std::string{"abc"} }
    });
}
Quirin F. Schroll
  • 1,302
  • 1
  • 11
  • 25