Как перемещать элементы initializer_list?
Скажем, у вас есть переменная типа std::vector<std::string>
, и вы инициализируете ее списком инициализаторов:
using V = std::vector<std::string>;
V v = { "Hello", "little", "world", "of", "move", "semantics" };
Компилятор создаст временный std::string
для каждого строкового литерала, создаст список инициализаторов по ним, а затем вызовет ctor для V
и создаст вектор. Ctor не знает, что все эти строки являются временными, поэтому копировать каждую строку.
Я не нашел ничего в стандарте, который позволяет вектору ctor перемещать элементы, когда они являются временными.
Я что-то упустил или с помощью списков инициализаций приводит к ненужным копиям? Я пишу классы, где эта проблема может привести к значительно неэффективному коду. Было бы полезно оценить любую технику, чтобы избежать ненужных копий.
Ответы
Ответ 1
Невозможно избежать копирования из initializer_list<string>
, поскольку стандарт определяет вызов конструктора, который принимает аргумент списка инициализатора, из инициализатора фигурных скобок в качестве фактического аргумента следующим образом (выделение добавлено):
С++ 14 §8.5.4/5
" Объект типа std::initializer_list<E>
создается из списка инициализаторов, как если бы реализация выделила временный массив элементов N
типа const E
, где N
- количество элементов в список инициализаторов
ИМХО это действительно неудачно.
Обходной путь (для ваших собственных классов) заключается в принятии initializer_list<char const*>
.
Вот пример обходного пути, применяемого к std::vector<string>
. Для этого, когда вы не контролируете код класса, он предполагает явно объявление массива данных (на самом деле initializer_list
). Это так же, как и для С++ 03, который должен был исключить механизм списка инициализаторов:
#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 ) );
}
Вывод:
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'
Ответ 2
После некоторого размышления я придумал решение на основе mutable
. Другой ответ по-прежнему в основном правилен, но можно создать прокси с изменяемым членом, чтобы избавиться от верхнего уровня const
-ness, а затем переместить элементы оттуда. Поэтому методы, использующие список инициализаторов, должны перегружать список инициализаторов const-ref и версию rvalue-ref, чтобы знать, когда им разрешено перемещаться.
Здесь рабочий пример, он может выглядеть произвольным сначала, но в моем реальном случае использования, он решил проблему.
#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"} } );
}
Живой пример