Могу ли я инициализировать список векторов типа "только перемещение"?

Если я пропущу следующий код через мой снимок GCC 4.7, он попытается скопировать unique_ptrs в вектор.

#include <vector>
#include <memory>

int main() {
    using move_only = std::unique_ptr<int>;
    std::vector<move_only> v { move_only(), move_only(), move_only() };
}

Очевидно, это не может сработать, потому что std::unique_ptr нельзя копировать:

ошибка: использование удаленной функции 'std :: unique_ptr ‹_Tp, _Dp> :: unique_ptr (const std :: unique_ptr‹ _Tp, _Dp> &) [с _Tp = int; _Dp = std :: default_delete; std :: unique_ptr ‹_Tp, _Dp> = std :: unique_ptr] '

Правильно ли GCC пытается скопировать указатели из списка инициализаторов?


person R. Martinho Fernandes    schedule 12.12.2011    source источник
comment
Visual Studio и clang ведут себя одинаково   -  person Jean-Simon Brochu    schedule 12.03.2018


Ответы (5)


Синопсис <initializer_list> в 18.9 достаточно ясно показывает, что элементы списка инициализаторов всегда передаются через константную ссылку. К сожалению, в текущей версии языка не существует способа использования семантики перемещения в элементах списка инициализаторов.

В частности, у нас есть:

typedef const E& reference;
typedef const E& const_reference;

typedef const E* iterator;
typedef const E* const_iterator;

const E* begin() const noexcept; // first element
const E* end() const noexcept; // one past the last element
person Kerrek SB    schedule 12.12.2011
comment
Рассмотрим идиому в ‹T›, описанную на cpptruths (cpptruths.blogspot.com/2013/09/). Идея состоит в том, чтобы определить lvalue / rvalue во время выполнения, а затем вызвать перемещение или копирование. in ‹T› обнаружит rvalue / lvalue, даже если стандартный интерфейс, предоставляемый initializer_list, является константной ссылкой. - person Sumant; 24.09.2013
comment
@Sumant Мне не кажется таким идиоматичным: не правда ли, чистый UB? поскольку не только итератор, но и сами базовые элементы могут быть const, которые нельзя отбросить в правильно сформированной программе. - person underscore_d; 18.07.2016

Изменить: поскольку @Johannes, похоже, не хочет публиковать лучшее решение в качестве ответа, я просто сделаю это.

#include <iterator>
#include <vector>
#include <memory>

int main(){
  using move_only = std::unique_ptr<int>;
  move_only init[] = { move_only(), move_only(), move_only() };
  std::vector<move_only> v{std::make_move_iterator(std::begin(init)),
      std::make_move_iterator(std::end(init))};
}

Итераторы, возвращаемые std::make_move_iterator, будут перемещать указанный элемент при разыменовании.


Исходный ответ: здесь мы воспользуемся небольшим вспомогательным типом:

#include <utility>
#include <type_traits>

template<class T>
struct rref_wrapper
{ // CAUTION - very volatile, use with care
  explicit rref_wrapper(T&& v)
    : _val(std::move(v)) {}

  explicit operator T() const{
    return T{ std::move(_val) };
  }

private:
  T&& _val;
};

// only usable on temporaries
template<class T>
typename std::enable_if<
  !std::is_lvalue_reference<T>::value,
  rref_wrapper<T>
>::type rref(T&& v){
  return rref_wrapper<T>(std::move(v));
}

// lvalue reference can go away
template<class T>
void rref(T&) = delete;

К сожалению, простой код здесь не работает:

std::vector<move_only> v{ rref(move_only()), rref(move_only()), rref(move_only()) };

Поскольку стандарт по какой-либо причине не определяет конструктор конвертирующей копии, подобный этому:

// in class initializer_list
template<class U>
initializer_list(initializer_list<U> const& other);

initializer_list<rref_wrapper<move_only>>, созданный brace-init-list ({...}), не будет преобразован в initializer_list<move_only>, который принимает vector<move_only>. Итак, здесь нам нужна двухэтапная инициализация:

std::initializer_list<rref_wrapper<move_only>> il{ rref(move_only()),
                                                   rref(move_only()),
                                                   rref(move_only()) };
std::vector<move_only> v(il.begin(), il.end());
person Xeo    schedule 12.12.2011
comment
Ах ... это rvalue аналог std::ref, нет? Может, это стоит называть std::rref. - person Kerrek SB; 12.12.2011
comment
IIRC вам нужен явно определенный конструктор копирования в move_only, потому что в спецификации сказано, что конструктор копирования определяется как удаленный, если класс содержит ссылочный член rvalue. РЕДАКТИРОВАТЬ: Ах, NVM, rref_wrapper<T> никогда не копируется в вашем случае, поэтому это не имеет значения. - person Johannes Schaub - litb; 13.12.2011
comment
Думаю, это не следует оставлять без упоминания в комментариях :) move_only m[] = { move_only(), move_only(), move_only() }; std::vector<move_only> v(std::make_move_iterator(m), std::make_move_iterator(m + 3));. - person Johannes Schaub - litb; 13.12.2011
comment
@Johannes: Иногда я просто ускользаю от простых решений. Хотя должен признаться, я пока не беспокоился об этих move_iterators. - person Xeo; 13.12.2011
comment
@Johannes: Кроме того, почему это не ответ? :) - person Xeo; 13.12.2011
comment
v.reserve(init.size()) сделано при использовании std::make_move_iterator? Не все итераторы могут предоставить количество элементов. Я придумал аналогичное решение, в котором использовалось только vec.reserve(arr.size()); for (auto &v: arr) vec.emplace_back(std::move(v)). - person Johan Lundberg; 03.11.2012
comment
@JohanLundberg: Я бы подумал, что это проблема QoI, но я не понимаю, почему он не может этого сделать. Stdlib VC ++, например, рассылает теги на основе категории итератора и использует std::distance для итераторов прямого или лучшего качества, а std::move_iterator адаптирует категорию базового итератора. В любом случае, хорошее и лаконичное решение. Может, выложить как ответ? - person Xeo; 03.11.2012
comment
Я бы не использовал ссылочный член rvalue, так как вы должны убедиться, что ссылка должна быть использована до того, как будет достигнута следующая точка последовательности. Мне кажется, что здесь все не так, что приводит к неопределенному поведению. Однако вы можете использовать изменяемый элемент, не являющийся ссылочным, для получения аналогичного поведения. Источник: stackoverflow.com/questions/4176328/ - person Jean-Bernard Jansen; 23.03.2014
comment
@JBJansen Не до следующей «точки последовательности» (концепции, которой больше не существует), а до конца полного выражения, как здесь. - person Xeo; 23.03.2014
comment
Можно ли улучшить лучшее решение от @Johannes, переместив большую часть оставшихся ужасов во вспомогательную функцию? template ‹typename MoveOnly, int N› std :: vector ‹MoveOnly› VectorOfMoveOnlyFromInitializerList (std :: array ‹MoveOnly, N› init) {вектор возврата ‹MoveOnly› (std :: make_move_iterator (std :: begin (init)), std :: make_move_iterator (std :: end (init))); } Затем используйте его как: std :: vector ‹move_only› v = VectorOfMoveOnlyFromInitializerList ‹move_only, 3› ({{move_only (), move_only (), move_only ()}}); Жаль, что мне не нужен ‹move_only, 3› в вызывающей программе, но, видимо, он не может определить тип без него. - person Don Hatch; 22.06.2016
comment
См. Также мой ответ, в котором используется make_array(). Можно ли сделать это более прямым? - person metal; 21.03.2017

Как упоминалось в других ответах, поведение std::initializer_list заключается в том, чтобы удерживать объекты по значению и не позволять перемещаться, поэтому это невозможно. Вот один из возможных обходных путей, использующий вызов функции, в которой инициализаторы задаются как переменные аргументы:

#include <vector>
#include <memory>

struct Foo
{
    std::unique_ptr<int> u;
    int x;
    Foo(int x = 0): x(x) {}
};

template<typename V>        // recursion-ender
void multi_emplace(std::vector<V> &vec) {}

template<typename V, typename T1, typename... Types>
void multi_emplace(std::vector<V> &vec, T1&& t1, Types&&... args)
{
    vec.emplace_back( std::move(t1) );
    multi_emplace(vec, args...);
}

int main()
{
    std::vector<Foo> foos;
    multi_emplace(foos, 1, 2, 3, 4, 5);
    multi_emplace(foos, Foo{}, Foo{});
}

К сожалению, multi_emplace(foos, {}); не работает, поскольку он не может определить тип для {}, поэтому для создания объектов по умолчанию вы должны повторить имя класса. (или используйте vector::resize)

person M.M    schedule 10.11.2015
comment
Рекурсивное расширение пакета может быть заменено взломом оператора запятой фиктивного массива, чтобы сэкономить пару строк кода. - person M.M; 10.11.2015

Обновление для C ++ 20: используя трюк Йоханнеса Шауба std::make_move_iterator() с std::to_array() C ++ 20, вы можете использовать вспомогательную функцию, такую ​​как make_tuple() и т. д., здесь она называется make_vector():

#include <array>
#include <memory>
#include <vector>

struct X {};

template<class T, std::size_t N>
auto make_vector( std::array<T,N>&& a )
    -> std::vector<T>
{
    return { std::make_move_iterator(std::begin(a)), std::make_move_iterator(std::end(a)) };
}

template<class... T>
auto make_vector( T&& ... t )
{
    return make_vector( std::to_array({ std::forward<T>(t)... }) );
}

int main()
{
    using UX = std::unique_ptr<X>;
    const auto a  = std::to_array({ UX{}, UX{}, UX{} });     // Ok
    const auto v0 = make_vector( UX{}, UX{}, UX{} );         // Ok
    //const auto v2 = std::vector< UX >{ UX{}, UX{}, UX{} }; // !! Error !!
}

Смотрите в прямом эфире на Godbolt.


Аналогичный ответ для более старого C ++:

Используя уловку Йоханнеса Шауба std::make_move_iterator() с std::experimental::make_array(), вы можете использовать вспомогательную функцию:

#include <memory>
#include <type_traits>
#include <vector>
#include <experimental/array>

struct X {};

template<class T, std::size_t N>
auto make_vector( std::array<T,N>&& a )
    -> std::vector<T>
{
    return { std::make_move_iterator(std::begin(a)), std::make_move_iterator(std::end(a)) };
}

template<class... T>
auto make_vector( T&& ... t )
    -> std::vector<typename std::common_type<T...>::type>
{
    return make_vector( std::experimental::make_array( std::forward<T>(t)... ) );
}

int main()
{
    using UX = std::unique_ptr<X>;
    const auto a  = std::experimental::make_array( UX{}, UX{}, UX{} ); // Ok
    const auto v0 = make_vector( UX{}, UX{}, UX{} );                   // Ok
    //const auto v1 = std::vector< UX >{ UX{}, UX{}, UX{} };           // !! Error !!
}

Смотрите в прямом эфире на Coliru.

Возможно, кто-то сможет использовать уловку std::make_array(), чтобы позволить make_vector() делать свое дело напрямую, но я не видел как (точнее, я пробовал то, что, по моему мнению, должно работать, потерпел неудачу и двинулся дальше). В любом случае компилятор должен иметь возможность встроить массив в векторное преобразование, как это делает Clang с O2 на GodBolt.

person metal    schedule 20.03.2017

Как было указано, невозможно инициализировать вектор типа «только перемещение» с помощью списка инициализаторов. Решение, изначально предложенное @Johannes, работает нормально, но у меня есть другая идея ... Что, если мы не создадим временный массив, а затем переместим элементы оттуда в вектор, а используем размещение new для инициализации этого массива уже вместо блок памяти вектора?

Вот моя функция для инициализации вектора unique_ptr с использованием пакета аргументов:

#include <iostream>
#include <vector>
#include <make_unique.h>  /// @see http://stackoverflow.com/questions/7038357/make-unique-and-perfect-forwarding

template <typename T, typename... Items>
inline std::vector<std::unique_ptr<T>> make_vector_of_unique(Items&&... items) {
    typedef std::unique_ptr<T> value_type;

    // Allocate memory for all items
    std::vector<value_type> result(sizeof...(Items));

    // Initialize the array in place of allocated memory
    new (result.data()) value_type[sizeof...(Items)] {
        make_unique<typename std::remove_reference<Items>::type>(std::forward<Items>(items))...
    };
    return result;
}

int main(int, char**)
{
    auto testVector = make_vector_of_unique<int>(1,2,3);
    for (auto const &item : testVector) {
        std::cout << *item << std::endl;
    }
}
person Gart    schedule 15.05.2013
comment
Это ужасная идея. Установка нового - это не молоток, это инструмент высокой точности. result.data() не является указателем на какую-то случайную память. Это указатель на объект. Подумайте, что происходит с этим плохим объектом, когда вы кладете на него новый. - person R. Martinho Fernandes; 15.05.2013
comment
Кроме того, форма размещения нового массива в действительности не используется stackoverflow.com/questions/8720425/ - person R. Martinho Fernandes; 15.05.2013
comment
@Р. Мартиньо Фернандес: спасибо за указание на то, что новое размещение для массивов не сработает. Теперь я понимаю, почему это была плохая идея. - person Gart; 15.05.2013