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.
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):
” An object of type
std::initializer_list<E>
is constructed from an initializer list as if the implementation allocated a temporary array ofN
elements of typeconst E
, whereN
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'
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With