initializer_list e sposta la semantica

Sono autorizzato a spostare elementi da una std::initializer_list ?

 #include  #include  template void foo(std::initializer_list list) { for (auto it = list.begin(); it != list.end(); ++it) { bar(std::move(*it)); // kosher? } } 

Dal momento che std::intializer_list richiede un’attenzione speciale al compilatore e non ha una semantica del valore come i normali contenitori della libreria standard C ++, preferirei essere al sicuro che scusarmi e chiedere.

No, non funzionerà come previsto; otterrai ancora delle copie. Sono abbastanza sorpreso da questo, dato che avevo pensato che initializer_list esistesse per mantenere una serie di provvisori finché non venivano move ‘d.

begin e end per initializer_list return const T * , quindi il risultato dello move nel codice è T const && – un riferimento di valore immutabile. Tale espressione non può essere spostata significativamente da. Si collegherà a un parametro di funzione di tipo T const & poiché i valori di rvalue si collegano ai riferimenti di const-value, e vedrai ancora la semantica della copia.

Probabilmente la ragione di ciò è che il compilatore può scegliere di rendere la lista di inizializzazione una costante inizializzata staticamente, ma sembra che sia più pulito rendere il suo tipo initializer_list o const initializer_list a discrezione del compilatore, quindi l’utente non sa se aspettarsi un risultato const o mutabile dall’inizio e alla end . Ma questo è solo il mio istinto, probabilmente c’è una buona ragione per cui mi sbaglio.

Aggiornamento: ho scritto una proposta ISO per il supporto initializer_list dei tipi di spostamento. È solo una prima bozza e non è ancora implementata da nessuna parte, ma puoi vederla per ulteriori analisi del problema.

 bar(std::move(*it)); // kosher? 

Non nel modo in cui tu intendi. Non è ansible spostare un object const . E std::initializer_list fornisce solo l’accesso const ai suoi elementi. Quindi il tipo di it è const T * .

Il tuo tentativo di chiamare std::move(*it) comporterà solo un valore l. IE: una copia.

std::initializer_list riferimento alla memoria statica . Ecco a cosa serve la class. Non puoi spostarti dalla memoria statica, perché il movimento implica cambiarlo. Puoi solo copiare da esso.

Questo non funzionerà come detto, perché list.begin() ha tipo const T * , e non c’è modo di spostarti da un object costante. I progettisti linguistici probabilmente lo hanno fatto per consentire agli elenchi di inizializzazione di contenere per esempio le costanti di stringa, dalle quali sarebbe inappropriato spostare.

Tuttavia, se ti trovi in ​​una situazione in cui sai che l’elenco di inizializzazione contiene espressioni rvalue (o vuoi forzare l’utente a scriverle), allora c’è un trucco che lo farà funzionare (sono stato ispirato dalla risposta di Sumant per questo, ma la soluzione è molto più semplice di quella). È necessario che gli elementi memorizzati nell’elenco di inizializzazione non siano valori T , ma valori che incapsulano T&& . Quindi anche se questi stessi valori sono qualificati, possono comunque recuperare un valore modificabile.

 template class rref_capture { T* ptr; public: rref_capture(T&& x) : ptr(&x) {} operator T&& () const { return std::move(*ptr); } // restitute rvalue ref }; 

Ora invece di dichiarare un argomento initializer_list , dichiari un argomento initializer_list > . Ecco un esempio concreto, che coinvolge un vettore di puntatori intelligenti std::unique_ptr , per il quale è definita solo la semantica mossa (quindi questi oggetti non possono mai essere memorizzati in una lista di inizializzatori); ancora la lista di inizializzazione sotto compila senza problemi.

 #include  #include  class uptr_vec { typedef std::unique_ptr uptr; // move only type std::vector data; public: uptr_vec(uptr_vec&& v) : data(std::move(v.data)) {} uptr_vec(std::initializer_list > l) : data(l.begin(),l.end()) {} uptr_vec& operator=(const uptr_vec&) = delete; int operator[] (size_t index) const { return *data[index]; } }; int main() { std::unique_ptr a(new int(3)), b(new int(1)),c(new int(4)); uptr_vec v { std::move(a), std::move(b), std::move(c) }; std::cout << v[0] << "," << v[1] << "," << v[2] << std::endl; } 

Una domanda ha bisogno di una risposta: se gli elementi della lista di inizializzazione devono essere veri valori (nell'esempio sono xvalori), la lingua assicura che la durata dei corrispondenti provvisori si estenda fino al punto in cui vengono utilizzati? Francamente, non penso che la sezione 8.5 pertinente dello standard risolva questo problema. Comunque, leggendo 1.9: 10, sembrerebbe che l' espressione completa rilevante in tutti i casi comprenda l'uso della lista di inizializzazione, quindi penso che non ci sia il rischio di far citare riferimenti di valore.

Sembra non consentito nello standard attuale come già risposto . Ecco un’altra soluzione per ottenere qualcosa di simile, definendo la funzione come variadic invece di prendere una lista di inizializzatori.

 #include  #include  // begin helper functions template  void add_to_vector(std::vector* vec) {} template  void add_to_vector(std::vector* vec, T&& car, Args&&... cdr) { vec->push_back(std::forward(car)); add_to_vector(vec, std::forward(cdr)...); } template  std::vector make_vector(Args&&... args) { std::vector result; add_to_vector(&result, std::forward(args)...); return result; } // end helper functions struct S { S(int) {} S(S&&) {} }; void bar(S&& s) {} template  void foo(Args&&... args) { std::vector args_vec = make_vector(std::forward(args)...); for (auto& arg : args_vec) { bar(std::move(arg)); } } int main() { foo(S(1), S(2), S(3)); return 0; } 

I modelli Variadic possono gestire i riferimenti di valore R in modo appropriato, diversamente da initializer_list.

In questo codice di esempio, ho usato un set di piccole funzioni di supporto per convertire gli argomenti variadici in un vettore, per renderlo simile al codice originale. Ma ovviamente puoi scrivere direttamente una funzione ricorsiva con modelli variadici.

Ho pensato che sarebbe stato istruttivo offrire un ragionevole punto di partenza per una soluzione alternativa.

Commenti in linea.

 #include  #include  #include  #include  #include  #include  template struct maker; // a maker which makes a std::vector template struct maker> { using result_type = std::vector; template auto operator()(Ts&&...ts) const -> result_type { result_type result; result.reserve(sizeof...(Ts)); using expand = int[]; void(expand { 0, (result.push_back(std::forward(ts)),0)... }); return result; } }; // a maker which makes std::array template struct maker> { using result_type = std::array; template auto operator()(Ts&&...ts) const { return result_type { std::forward(ts)... }; } }; // // delegation function which selects the correct maker // template auto make(Ts&&...ts) { auto m = maker(); return m(std::forward(ts)...); } // vectors and arrays of non-copyable types using vt = std::vector>; using at = std::array,2>; int main(){ // build an array, using make<> for consistency auto a = make(std::make_unique(10), std::make_unique(20)); // build a vector, using make<> because an initializer_list requires a copyable type auto v = make(std::make_unique(10), std::make_unique(20)); } 

Considera l’idioma in descritto su cpptruths . L’idea è di determinare lvalue / rvalue in fase di esecuzione e quindi chiamare move o copy-construction. in rileverà rvalue / lvalue anche se l’interfaccia standard fornita da initializer_list è const riferimento.