¿Puedo inicializar un vector de tipo move-only?


Si paso el siguiente código a través de mi instantánea GCC 4.7, intenta copiar las unique_ptrs en el vector.

#include <vector>
#include <memory>

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

Obviamente eso no puede funcionar porque std::unique_ptr no es copiable:

Error: uso de la función eliminada ' std:: unique_ptr<_tp>:: unique_ptr(const std:: unique_ptr<_tp>&) [con _Tp = int; _Dp = std::default_delete; std::unique_ptr<_tp> = std:: unique_ptr]'

¿Es GCC correcto al intentar copiar los punteros de la lista inicializador?

Author: R. Martinho Fernandes, 2011-12-12

5 answers

La sinopsis de <initializer_list> en 18.9 deja razonablemente claro que los elementos de una lista inicializadora siempre se pasan a través de const-reference. Desafortunadamente, no parece haber ninguna forma de usar move-semantic en los elementos de la lista inicializer en la revisión actual del lenguaje.

Específicamente, tenemos:

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
 40
Author: Kerrek SB,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-12-12 00:55:54

Edit: Dado que @Johannes no parece querer publicar la mejor solución como respuesta, simplemente lo haré.

#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))};
}

Los iteradores devueltos por std::make_move_iterator moverán el elemento apuntado al ser desreferenciado.


Respuesta original: Vamos a utilizar un pequeño tipo de ayudante aquí:

#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;

Lamentablemente, el código directo aquí no funcionará:

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

Dado que el estándar, por cualquier razón, no define una copia de conversión constructor como este:

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

El initializer_list<rref_wrapper<move_only>> creado por la lista de inicio de llaves ({...}) no se convertirá en el initializer_list<move_only> que toma el vector<move_only>. Así que necesitamos una inicialización de dos pasos aquí:

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());
 51
Author: Xeo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-03-08 15:50:08

Como se mencionó en otras respuestas, el comportamiento de std::initializer_list es mantener objetos por valor y no permitir que se muevan, por lo que esto no es posible. Aquí hay una posible solución, usando una llamada a función donde los inicializadores se dan como argumentos variádicos:

#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{});
}

Desafortunadamente multi_emplace(foos, {}); falla ya que no puede deducir el tipo para {}, por lo que para que los objetos se construyan por defecto, debe repetir el nombre de la clase. (o utilizar vector::resize)

 7
Author: M.M,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-11-10 01:03:47

Usando el truco de Johannes Schaub de std::make_move_iterator() con std::experimental::make_array(), puede usar una función auxiliar:

#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 !!
}

Véalo en vivoColiru.

Tal vez alguien puede aprovechar std::make_array()'s truco para permitir make_vector() para hacer su cosa directamente, pero no vi cómo (más exactamente, traté de lo que pensé que debería funcionar, falló, y se trasladó). En cualquier caso, el compilador debería ser capaz de alinear la matriz a la transformación vectorial, como lo hace Clang con O2 en GodBolt.

 0
Author: metal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-21 12:44:08

Como se ha señalado, no es posible inicializar un vector de tipo move-only con una lista inicializadora. La solución propuesta originalmente por @ Johannes funciona bien, pero tengo otra idea... ¿Qué pasa si no creamos una matriz temporal y luego movemos elementos desde allí al vector, pero usamos la colocación new para inicializar esta matriz ya en lugar del bloque de memoria del vector?

Aquí está mi función para inicializar un vector de unique_ptr usando un paquete de argumentos:

#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;
    }
}
 -1
Author: Gart,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-05-15 11:36:55