Reputation: 56921
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.
Upvotes: 18
Views: 2845
Reputation: 1697
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"} }
});
}
Upvotes: 1
Reputation: 56921
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"} } );
}
Upvotes: 4
Reputation: 145457
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'
Upvotes: 7