Posso inizializzare da elenco un vettore di tipo solo spostamento?


95

Se passo il codice seguente attraverso la mia istantanea GCC 4.7, prova a copiare i messaggi di posta unique_ptrelettronica nel vettore.

#include <vector>
#include <memory>

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

Ovviamente non può funzionare perché std::unique_ptrnon è copiabile:

errore: utilizzo della funzione cancellata 'std :: unique_ptr <_Tp, _Dp> :: unique_ptr (const std :: unique_ptr <_Tp, _Dp> &) [con _Tp = int; _Dp = std :: default_delete; std :: unique_ptr <_Tp, _Dp> = std :: unique_ptr] '

GCC è corretto nel tentativo di copiare i puntatori dall'elenco degli inizializzatori?


Visual Studio e clang hanno lo stesso comportamento
Jean-Simon Brochu

Risposte:


46

La sinossi della <initializer_list>18.9 rende ragionevolmente chiaro che gli elementi di un elenco di inizializzatori vengono sempre passati tramite const-reference. Sfortunatamente, non sembra esserci alcun modo per utilizzare la semantica di spostamento negli elementi dell'elenco di inizializzatori nella revisione corrente del linguaggio.

Nello specifico abbiamo:

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

4
Considera l'idioma in <T> descritto su cpptruths ( cpptruths.blogspot.com/2013/09/… ). L'idea è di determinare lvalue / rvalue in fase di esecuzione e quindi chiamare move o copy-construction. in <T> rileverà rvalue / lvalue anche se l'interfaccia standard fornita da initializer_list è un riferimento const.
Sumant

3
@Sumant Non mi sembra così "idiomatico": non è invece puro UB? come potrebbero essere non solo l'iteratore ma piuttosto gli stessi elementi sottostanti const, che non possono essere eliminati in un programma ben formato.
underscore_d

62

Modifica: poiché @Johannes non sembra voler pubblicare la soluzione migliore come risposta, lo farò e basta.

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

Gli iteratori restituiti da std::make_move_iteratorsposteranno l'elemento puntato quando viene dereferenziato.


Risposta originale: utilizzeremo un piccolo tipo di aiuto qui:

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

Purtroppo, il codice semplice qui non funzionerà:

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

Poiché lo standard, per qualsiasi motivo, non definisce un costruttore di copie di conversione come questo:

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

Il initializer_list<rref_wrapper<move_only>>creato da parentesi graffa-init-list ( {...}) non verrà convertito in quello initializer_list<move_only>che vector<move_only>prende. Quindi abbiamo bisogno di un'inizializzazione in due passaggi qui:

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());

1
Ah ... questo è l'analogo rvalue di std::ref, no? Forse dovrebbe essere chiamato std::rref.
Kerrek SB

17
Ora, immagino che questo non dovrebbe essere lasciato senza essere menzionato in un commento :) 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 volte, sono le soluzioni semplici che mi sfuggono. Anche se devo ammetterlo, non mi sono ancora preoccupato di quelle move_iterator.
Xeo

2
@Johannes: Inoltre, perché non è una risposta? :)
Xeo

1
@JohanLundberg: lo considererei un problema di QoI, ma non vedo perché non possa farlo. Lo stdlib di VC ++, ad esempio, invia tag basati sulla categoria dell'iteratore e utilizza std::distanceper gli iteratori forward-or-better e std::move_iteratoradatta la categoria dell'iteratore sottostante. Comunque, buona e concisa soluzione. Postalo come risposta, forse?
Xeo

10

Come accennato in altre risposte, il comportamento di std::initializer_listè quello di tenere gli oggetti in base al valore e non consentire lo spostamento, quindi questo non è possibile. Ecco una possibile soluzione alternativa, utilizzando una chiamata di funzione in cui gli inizializzatori vengono forniti come argomenti variadici:

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

Sfortunatamente multi_emplace(foos, {});fallisce in quanto non può dedurre il tipo per {}, quindi per gli oggetti da costruire in modo predefinito devi ripetere il nome della classe. (o usa vector::resize)


4
L'espansione ricorsiva del pacchetto potrebbe essere sostituita dall'hack dell'operatore virgola di matrice fittizia, per salvare un paio di righe di codice
MM

0

Usando il trucco del std::make_move_iterator()con di Johannes Schaub std::experimental::make_array(), puoi usare una funzione di supporto:

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

Guardalo dal vivo Coliru.

Forse qualcuno può sfruttare std::make_array()l'inganno per permettere make_vector()di fare le sue cose direttamente, ma non ho visto come (più precisamente, ho provato quello che pensavo dovrebbe funzionare, ho fallito e sono andato avanti). In ogni caso, il compilatore dovrebbe essere in grado di integrare la trasformazione da array a vettore, come fa Clang con O2 attivo GodBolt.


-1

Come è stato sottolineato, non è possibile inizializzare un vettore di tipo solo movimento con una lista di inizializzatori. La soluzione originariamente proposta da @Johannes funziona bene, ma ho un'altra idea ... E se non creiamo un array temporaneo e poi spostiamo gli elementi da lì nel vettore, ma usiamo il posizionamento newper inizializzare questo array già al posto del blocco di memoria del vettore?

Ecco la mia funzione per inizializzare un vettore di unique_ptrusando un pacchetto di argomenti:

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

Questa è un'idea terribile. Il posizionamento nuovo non è un martello, è uno strumento di alta precisione. result.data()non è un puntatore a una memoria casuale. È un puntatore a un oggetto . Pensa a cosa succede a quel povero oggetto quando lo metti di nuovo sopra.
R. Martinho Fernandes

Inoltre, la forma dell'array di posizionamento new non è realmente utilizzabile stackoverflow.com/questions/8720425/…
R. Martinho Fernandes

@R. Martinho Fernandes: grazie per aver sottolineato che il posizionamento nuovo per gli array non funzionerebbe. Ora capisco perché è stata una cattiva idea.
Gart
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.