Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how to move elements of an initializer_list?

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.

like image 295
Daniel Frey Avatar asked Apr 02 '16 19:04

Daniel Frey


2 Answers

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'
like image 142
Cheers and hth. - Alf Avatar answered Nov 01 '22 10:11

Cheers and hth. - Alf


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

like image 4
Daniel Frey Avatar answered Nov 01 '22 09:11

Daniel Frey