Использование семантики перемещения и поэлементной инициализации контейнеров

Часто видно, что примеры использования алгоритмов STL иллюстрируются Список инициализируется контейнеры, такие как:

std::vector< int > v{1, 2, 3, 4};

Но когда этот подход используется для (тяжеловес) классы (В отличие от ints) это подразумевает чрезмерные операции копирования, даже если они переданы Rvalue (переехал в), так как std::initializer_list используется в приведенном выше примере обеспечивает только const_iterators.

Для решения этой проблемы я использую следующий (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)?

4

Решение

Есть лучшее решение:

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на этот раз это простой массив, так что вы можете переместить элементы из него.

Заметные преимущества по сравнению с вашим оригинальным решением:

  • Лучшая производительность: это вызывает ContainerCtor напрямую, который может дать лучшую производительность (например, std::vector можно зарезервировать всю необходимую память)
  • Гарантия заказа: это инициализация списка

DEMO

1

Другие решения

Это совершенно неправильный подход с какой-то конкретной точки зрения?

Это излишне сложно понять и может плохо работать, если вызываемая функция перемещается перед копированием. Помните, 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)};
1