Как переместить элементы 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 перемещать элементы, когда они временные.
Я что-то упустил или использование списков инициализаторов приводит к ненужным копиям? Я пишу классы, где эта проблема может привести к значительной неэффективности кода. Любая техника, чтобы избежать ненужных копий будет принята с благодарностью.
3 ответа
Нет способа избежать копирования с initializer_list<string>
поскольку стандарт определяет вызов конструктора, принимающего аргумент списка инициализатора, из инициализатора фигурных скобок в качестве фактического аргумента следующим образом (выделение добавлено):
Объект типа
std::initializer_list<E>
построен из списка инициализатора, как если бы реализация выделяла временный массивN
элементы типаconst E
, гдеN
количество элементов в списке инициализатора
ИМХО это действительно неудачно.
Обходной путь (для ваших собственных классов) - принять initializer_list<char const*>
,
Вот пример обходного пути, примененного к std::vector<string>
, Для этого, когда вы не управляете кодом класса, это включает объявление массива данных (на самом деле initializer_list
) явно. Это так же, как в C++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 ) );
}
Выход:
Создание вектора а. My_string(*) <- 'a1' My_string (*) <- 'a2' My_string (*) <- 'a3' My_string (const &) <- 'a1' My_string (const &) <- 'a2' My_string (const &) <- 'a3' Создание данных для вектора б. Создание вектора б. My_string(*) <- 'b1' My_string (*) <- 'b2' My_string (*) <- 'b3'
Подумав, я нашел решение, основанное на mutable
, Другой ответ по-прежнему в основном правильный, но можно создать прокси с изменяемым членом, чтобы избавиться от верхнего уровня. const
и затем переместить элементы оттуда. Поэтому методы, использующие список инициализаторов, должны перегружаться для списка инициализаторов 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"} } );
}
Вместо использования списка инициализаторов за стоимость шаблона вы можете взять ссылку rvalue на массив:
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));
}
(Параметр шаблонаT
для техники не обязательно, если вы знаете тип, ноn
является.)
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"} }
});
}