Часто видно, что примеры использования алгоритмов STL иллюстрируются Список инициализируется контейнеры, такие как:
std::vector< int > v{1, 2, 3, 4};
Но когда этот подход используется для (тяжеловес) классы (В отличие от int
s) это подразумевает чрезмерные операции копирования, даже если они переданы Rvalue (переехал в), так как std::initializer_list
используется в приведенном выше примере обеспечивает только const_iterator
s.
Для решения этой проблемы я использую следующий (C ++ 17) подход:
template< typename Container, typename ...Args >
Container make_container(Args &&... args)
{
Container c;
(c.push_back(std::forward< Args >(args)), ...);
// ((c.insert(std::cend(c), std::forward< Args >(args)), void(0)), ...); // more generic approach
return c;
}
auto u = make_container< std::vector< A > >(A{}, A{}, A{});
Но это становится неудовлетворительным, когда я делаю следующее:
A a;
B b;
using P = std::pair< A, B >;
auto v = make_container< std::vector< P > >(P{a, b}, P{std::move(a), std::move(b)});
Здесь я хочу сохранить одну операцию копирования на значение с помощью операции замены копии операцией перемещения (предположим, для перемещения A
или же B
намного дешевле, чем копировать), но обычно не может, потому что порядок вычисления аргументов функции не определен в C ++. Мое текущее решение:
template< Container >
struct make_container
{
template< typename ...Args >
make_container(Args &&... args)
{
(c.push_back(std::forward< Args >(args)), ...);
}
operator Container () && { return std::move(c); }
private :
Container c;
};
A a; B b;
using P = std::pair< A, B >;
using V = std::vector< P >;
V w = make_container< V >{P{a, b}, P{std::move(a), std::move(b)}};
Часто считается плохой практикой делать какую-то нетривиальную работу в телах конструкторов, но здесь я интенсивно использовал само свойство Список инициализация — тот факт, что это строго слева направо.
Это совершенно неправильный подход с какой-то конкретной точки зрения? Каковы недостатки этого подхода, кроме упомянутого выше? Есть ли другой метод для достижения предсказуемого порядка вычисления аргументов функции в настоящее время (в C ++ 11, C ++ 14, C ++ 1z)?
Есть лучшее решение:
template<class Container, std::size_t N>
inline Container make_container(typename Container::value_type (&&a)[N])
{
return Container(std::make_move_iterator(std::begin(a)), std::make_move_iterator(std::end(a)));
}
Вы можете использовать это так:
make_container<std::vector<A>>({A(1), A(2)})
Он не нуждается в шаблоне с переменным номером и все еще инициализируется списком, но не std::initializer_list
на этот раз это простой массив, так что вы можете переместить элементы из него.
Заметные преимущества по сравнению с вашим оригинальным решением:
Container
Ctor напрямую, который может дать лучшую производительность (например, std::vector
можно зарезервировать всю необходимую память)Это совершенно неправильный подход с какой-то конкретной точки зрения?
Это излишне сложно понять и может плохо работать, если вызываемая функция перемещается перед копированием. Помните, std::move
не двигается. Это позволяет движется. Ничего более. Это вызванная функция, которая в конечном итоге движется. Если Это обрабатывает параметры слева направо, это будет работать. Если нет, то нет.
Дайте понять, что происходит.
A a; A b;
using V = std::vector< A >;
A c {a};
V v = make_container< V >{std::move(a), b, std::move(c)};