¿Puedo inicializar en lista un vector de tipo de solo movimiento?

95

Si paso el siguiente código a través de mi instantánea de GCC 4.7, intenta copiar los 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_ptrno se puede copiar:

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

¿GCC tiene razón al intentar copiar los punteros de la lista de inicializadores?

R. Martinho Fernandes
fuente
Visual Studio y clang tienen el mismo comportamiento
Jean-Simon Brochu

Respuestas:

46

La sinopsis de <initializer_list>en 18.9 deja razonablemente claro que los elementos de una lista de inicializadores 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 de inicializadores 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
Kerrek SB
fuente
4
Considere el idioma in <T> descrito en cpptruths ( cpptruths.blogspot.com/2013/09/… ). La idea es determinar lvalue / rvalue en tiempo de ejecución y luego llamar a move o copy-construction. en <T> detectará rvalue / lvalue aunque la interfaz estándar proporcionada por initializer_list sea una referencia constante.
Sumant
3
@Sumant No me parece tan "idiomático": ¿no es, en cambio, pura UB? como no solo el iterador, sino los elementos subyacentes en sí mismos const, que no pueden descartarse en un programa bien formado.
underscore_d
62

Editar: 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_iteratormoverán el elemento apuntado cuando se desreferencia.


Respuesta original: vamos a utilizar un pequeño tipo de ayuda 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 sencillo aquí no funcionará:

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

Dado que el estándar, por la razón que sea, no define un constructor de copia de conversión 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 brace-init-list ( {...}) no se convertirá al initializer_list<move_only>que vector<move_only>toma. Entonces, 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());
Xeo
fuente
1
Ah ... este es el análogo de rvalue std::ref, ¿no? Quizás debería llamarse std::rref.
Kerrek SB
17
Ahora, supongo que esto no debería dejarse sin mencionarlo en un comentario :) 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));.
Johannes Schaub - litb
1
@Johannes: A veces, son las soluciones simples las que simplemente se me escapan. Aunque debo admitir que no me molesté con esos move_iteratormensajes todavía.
Xeo
2
@Johannes: Además, ¿por qué no es esa una respuesta? :)
Xeo
1
@JohanLundberg: Lo consideraría un problema de QoI, pero no veo por qué no podría hacer eso. El stdlib de VC ++, por ejemplo, distribuye etiquetas basadas en la categoría de iterador y se utiliza std::distancepara iteradores de avance o mejor y std::move_iteratoradapta la categoría del iterador subyacente. De todos modos, buena y concisa solución. Publicarlo como respuesta, ¿quizás?
Xeo
10

Como se mencionó en otras respuestas, el comportamiento de std::initializer_listes sostener 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 de función donde los inicializadores se dan como argumentos variadic:

#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 {}, por lo que para que los objetos se construyan por defecto, debe repetir el nombre de la clase. (o usar vector::resize)

MM
fuente
4
La expansión del paquete recursivo podría ser reemplazada por el hack del operador de coma de matriz ficticia, para guardar un par de líneas de código
MM
0

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 vivo Coliru.

Quizás alguien pueda aprovechar std::make_array()los trucos para permitir make_vector()hacer lo suyo directamente, pero no vi cómo (más exactamente, intenté lo que pensé que debería funcionar, fallé y seguí adelante). En cualquier caso, el compilador debería poder integrar la matriz a la transformación vectorial, como lo hace Clang con O2 activado GodBolt.

metal
fuente
-1

Como se ha señalado, no es posible inicializar un vector de tipo de solo movimiento con una lista de inicializadores. 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 ubicación newpara inicializar esta matriz ya en lugar de la bloque de memoria del vector?

Aquí está mi función para inicializar un vector de unique_ptrusando 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;
    }
}
Gart
fuente
Esa es una idea terrible. La colocación nueva no es un martillo, es una herramienta de fina precisión. result.data()no es un puntero a una memoria aleatoria. Es un puntero a un objeto . Piense en lo que le sucede a ese pobre objeto cuando coloca uno nuevo sobre él.
R. Martinho Fernandes
Además, la forma de matriz de colocación nueva no es realmente utilizable stackoverflow.com/questions/8720425/…
R. Martinho Fernandes
@R. Martinho Fernandes: gracias por señalar que la colocación nueva para matrices no funcionaría. Ahora veo por qué fue una mala idea.
Gart