2016-07-19 44 views
7

Considerare sottostante Codice:Qual è il modo migliore per inizializzare un contenitore con oggetti che sono a buon mercato per muoversi, ma pesante per copiare

#include <iostream> 
#include <vector> 

struct C { 
    C() {} 
    C(const C&) { std::cout << "A copy was made.\n"; } 
    C(C&&) {std::cout << "A move was made.\n";} 
}; 

std::vector<C> g() { 
    std::vector<C> ret {C(), C(), C()}; 
    return ret; 
} 

std::vector<C> h() { 
    std::vector<C> ret; 
    ret.reserve(3); 
    ret.push_back(C()); 
    ret.push_back(C()); 
    ret.push_back(C()); 
    return ret; 
} 

int main() { 
    std::cout << "Test g\n"; 
    std::vector<C> v1 = g(); 

    std::cout << "Test h\n"; 
    std::vector<C> v2 = h(); 
} 

Compilato con g++ -std=c++11 main.cpp && ./a.out, il risultato è:

Test g 
A copy was made. 
A copy was made. 
A copy was made. 
Test h 
A move was made. 
A move was made. 
A move was made. 

Nota che entrambe le funzioni utilizzano la copia elision in modo che lo std::vector<C> restituito non venga copiato.

Capisco perché h() utilizza move-constructor, ma perché g() utilizza copy-constructor?

Da vector's doc:

(6) lista di inizializzazione del costruttore

Costrutti un contenitore con una copia di ciascuno degli elementi il, nello stesso ordine.

Sembra di inizializzazione-list sempre copiare gli elementi, allora probabilmente significa prestazioni 's il initializer-list constructor potrebbe essere influenzato se C è a buon mercato per muoversi, ma pesante da copiare.

Quindi la mia domanda: qual è il modo preferito per inizializzare un contenitore (ad esempio vector) con oggetti economici da spostare ma pesanti da copiare?

+4

'std :: vector ritorno (3);'. E trova un modo per rendere 'noexcept' il costruttore di mosse' C'. – ildjarn

+2

Hai bisogno di fornire argomenti nel tuo codice reale o stai semplicemente usando il costruttore predefinito? – TartanLlama

+0

@ildjarn @TartanLlama Conosco 'std :: vector (3);' evita qualsiasi copia o spostamento, ma se voglio inizializzarmi con oggetti con parametri (non costruttore di default), non posso usare 'std :: vector (3); '. Quindi in tal caso, qual è il modo preferito? – Mine

risposta

3

È possibile passare da un elenco di inizializzazione con un po 'di piastra di riscaldamento.

template<class T> 
struct force_move{ 
    mutable T t; 

    template<class...Args> 
    force_move(Args&&...args): 
    t(std::forward<Args>(args)...) 
    {} 
    // todo: code that smartly uses {} if() does not work? 

    force_move()=default; 
    force_move(force_move const&)=delete; 

    template<class U, class...Args> 
    force_move(std::initializer_list<U> il, Args&&...args): 
    t(il, std::forward<Args>(args)...) 
    {} 

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

template<class T> 
struct make_container { 
    std::initializer_list<force_move<T>> il; 
    make_container(std::initializer_list<force_move<T>> l):il(l) {} 

    template<class C> 
    operator C()&&{ 
    return {il.begin(), il.end()}; 
    } 
}; 

Usa:

std::vector<C> v=make_container<C>{ {}, {} }; 

Questa è conciso, efficiente, e risolve il problema.

(Possibilmente dovrebbe essere operator T&& sopra. Non sono sicuro, e io sono cauto di tornare mai un riferimento rvalue ...)

Ora, questo sembra un po 'di hack. Ma le alternative fanno schifo.

L'elenco di back-emplace back manuale è brutto e diventa più brutto dopo aver aggiunto i requisiti di riserva per la massima efficienza. E la soluzione ingenua non può muoversi.

Soluzioni che non consentono di elencare gli elementi proprio lì dove l'istanza è dichiarata sono scomode, a mio parere. Vuoi la possibilità di mettere l'elenco dei contenuti adiacente alla dichiarazione.

Un'altra alternativa "elenco locale" consiste nel creare una funzione variardica che inizializza internamente uno std::array (possibilmente di wra wrappers) che quindi si sposta da tale array nel contenitore. Questo non consente gli elenchi di stili { {}, {}, {} }, quindi, lo trovo carente.

Potremmo fare questo:

template<class T, std::size_t N> 
std::vector<T> move_from_array(T(&arr)[N]){ 
    return {std::make_move_iterator(std::begin(arr)), std::make_move_iterator(std::end(arr))}; 
} 

Poi:

C arr[]={{}, {}, {}}; 
std::vector<C> v = move_from_array(arr); 

l'unico aspetto negativo è che richiedono due affermazioni al punto di utilizzo. Ma il codice è meno ottuso della mia prima soluzione.

+0

'force_move' e' make_container' funzionano, ma 'std :: vector v3 = make_container {C(), C(), C()};' costa 9 sposta le chiamate del costruttore. Quindi forse "move_from_array' potrebbe fare un lavoro migliore? – Mine

+0

@Mine Non 'C()', solo '{}'. Questo salva 3 mosse: hai creato i provvisori senza motivo. Sostituisci l'operatore C() 'con l'operatore C &&()'. Questo salva 3 mosse. – Yakk

+0

Cambia in '{}' salva 3 mosse, ma interessante, 'l'operatore C() &&' su clang dà 3 mosse, ma gcc dà 6 mosse. – Mine

2

Sfortunatamente std::initializer_listdoesn't really work with move semantics.

Se è necessario fornire argomenti al costruttore, vorrei invece usare emplace_back, che costruisce l'elemento al suo posto:

std::vector<C> h() { 
    std::vector<C> ret; 
    ret.reserve(3); 
    ret.emplace_back(arg1); 
    ret.emplace_back(arg2,arg3); 
    ret.emplace_back(0,12); 
    return ret; 
} 
+1

non dimenticare che i costruttori chiamati con 'emplace_back' devono essere' noexcept'. Altrimenti, verranno chiamati i costruttori di default + copy. –

+0

@AndreiR .: Impossibile riprodurre la copia aggiuntiva: [Demo] (http://coliru.stacked-crooked.com/a/3074b357819752ac) – Jarod42

+0

@ Jarod42, questo perché 'reserve' influisce anche sul comportamento: [demo] (http : //coliru.stacked-crooked.com/a/32d2cc583df98abe). In generale, spostare i costruttori/gli operatori di assegnazioni devono essere non eccetto lavorare con molti algoritmi ('std :: move_if_noexcept' è ampiamente usato in stl) –

3

È cannot move from an initializer_list (salvo ginnastica con mutable come nella risposta di Yakk) perché gli elementi di un inizializzatore_elenco viene dichiarato const (moving elements of an initialization_list considered dangerous?).

consiglierei costruire gli oggetti in un contenitore con inizializzazione aggregato cioè una matrice classica o std::array, quindi costruire il vettore da move iterators:

std::vector<C> h() { 
    C[] arr{C(), C(), C()}; 
    return std::vector<C>(
     std::make_move_iterator(std::begin(arr)), 
     std::make_move_iterator(std::end(arr))); 
} 
+0

In realtà la risposta di Yakk sembra essere ok. Non si sta spostando da 'initializer_list', ma sta solo convertendo gli elementi di esso. Sotto il cofano, i contenuti di initializer_list vengono modificati. Ma va bene, dato che i suoi elementi non possono essere messi in memoria di sola lettura, perché "mutable" lo impedisce. –

+0

@RalphTandetzky scusate, non volevo insinuare che la risposta di Yakk fosse sbagliata; solo che è piuttosto complicato (sebbene riutilizzabile) e che senza di esso si avrebbe un comportamento illegale. – ecatmur

Problemi correlati