initializer_list e sposta la semantica


97

Posso spostare elementi da un std::initializer_list<T>?

#include <initializer_list>
#include <utility>

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

Poiché std::intializer_list<T>richiede un'attenzione speciale da parte del compilatore e non ha una semantica dei valori come i normali contenitori della libreria standard C ++, preferirei essere prudente che dispiaciuto e chiedere.


I definisce linguaggio di base che l'oggetto a cui un initializer_list<T>sono non -const. Come, si initializer_list<int>riferisce agli intoggetti. Ma penso che sia un difetto: è inteso che i compilatori possono allocare staticamente un elenco nella memoria di sola lettura.
Johannes Schaub - litb

Risposte:


89

No, non funzionerà come previsto; riceverai comunque delle copie. Sono piuttosto sorpreso da questo, poiché pensavo che initializer_listesistesse per mantenere una serie di provvisori fino a quando non fossero stati eliminati move.

begine endper la initializer_listrestituzione const T *, quindi il risultato di movenel codice è T const &&- un riferimento rvalue immutabile. Un'espressione del genere non può essere spostata in modo significativo. Si T const &collegherà a un parametro di funzione di tipo perché rvalues ​​si associa a riferimenti const lvalue e vedrai ancora la semantica della copia.

Probabilmente la ragione è che il compilatore può scegliere di creare initializer_listuna costante inizializzata staticamente, ma sembra che sarebbe più pulito crearne il tipo initializer_listoa const initializer_listdiscrezione del compilatore, quindi l'utente non sa se aspettarsi a consto mutabile risultato da begine end. Ma questo è solo il mio istinto, probabilmente c'è una buona ragione per cui mi sbaglio.

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


11
Nel caso in cui non sia chiaro, significa comunque che l'uso std::moveè sicuro, se non produttivo. (Escludendo T const&&mosse costruttori.)
Luc Danton

Non penso che tu possa sostenere l'intera discussione const std::initializer_list<T>o semplicemente std::initializer_list<T>in un modo che non causi sorprese abbastanza spesso. Considera che ogni argomento nel initializer_listpuò essere consto no e questo è noto nel contesto del chiamante, ma il compilatore deve generare solo una versione del codice nel contesto del chiamato (cioè al foosuo interno non sa nulla degli argomenti che il chiamante sta passando)
David Rodríguez - dribeas

1
@ David: Buon punto, ma sarebbe comunque utile che un std::initializer_list &&sovraccarico faccia qualcosa, anche se è richiesto anche un sovraccarico senza riferimento. Suppongo che sarebbe ancora più confuso della situazione attuale, che è già grave.
Potatoswatter

1
@JBJansen Non può essere hackerato. Non vedo esattamente cosa dovrebbe realizzare quel codice rispetto a initializer_list, ma come utente non hai le autorizzazioni necessarie per spostarti da esso. Il codice sicuro non lo farà.
Potatoswatter

1
@ Potatoswatter, commento in ritardo, ma qual è lo stato della proposta. C'è qualche remota possibilità che possa entrare in C ++ 20?
WhiZTiM

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

Non nel modo in cui intendi. Non puoi spostare un constoggetto. E std::initializer_listfornisce solo l' constaccesso ai suoi elementi. Quindi il tipo di itè const T *.

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

std::initializer_listfa riferimento alla memoria statica . Ecco a cosa serve la classe. Non puoi muoverti dalla memoria statica, perché il movimento implica cambiarla. Puoi solo copiare da esso.


Un const xvalue è ancora un xvalue e, initializer_listse necessario , fa riferimento allo stack. (Se il contenuto non è costante, è comunque thread-safe.)
Potatoswatter

5
@ Potatoswatter: non puoi muoverti da un oggetto costante. L' initializer_listoggetto stesso può essere un xvalue, ma i suoi contenuti (l'effettivo array di valori a cui punta) lo sono const, perché quei contenuti possono essere valori statici. Semplicemente non puoi spostarti dal contenuto di un file initializer_list.
Nicol Bolas

Vedi la mia risposta e la sua discussione. Ha spostato l'iteratore dereferenziato, producendo un constxvalue. movepotrebbe non avere significato, ma è legale e persino possibile dichiarare un parametro che accetta proprio questo. Se lo spostamento di un particolare tipo risulta essere un no-op, potrebbe anche funzionare correttamente.
Potatoswatter

1
@ Potatoswatter: lo standard C ++ 11 richiede molto linguaggio per garantire che gli oggetti non temporanei non vengano effettivamente spostati a meno che non vengano utilizzati std::move. Ciò garantisce che si possa capire dall'ispezione quando si verifica un'operazione di spostamento, poiché influisce sia sull'origine che sulla destinazione (non si desidera che avvenga implicitamente per gli oggetti denominati). Per questo motivo , se si utilizza std::movein un luogo in cui non vieneconst eseguita un'operazione di spostamento (e non si verificherà alcun movimento effettivo se si dispone di un valore x), il codice è fuorviante. Penso che sia un errore std::moveessere richiamabile su un constoggetto.
Nicol Bolas

1
Forse, ma prenderò comunque meno eccezioni alle regole sulla possibilità di codice fuorviante. Ad ogni modo, questo è esattamente il motivo per cui ho risposto "no" anche se è legale, e il risultato è un xvalue anche se si legherà solo come const lvalue. Ad essere onesti, ho già avuto un breve flirt con const &&in una classe raccolta dalla spazzatura con puntatori gestiti, dove tutto ciò che era rilevante era mutabile e lo spostamento spostava la gestione del puntatore ma non influiva sul valore contenuto. Ci sono sempre casi limite difficili: v).
Potatoswatter

2

Questo non funzionerà come indicato, perché list.begin()ha il tipo const T *e non è possibile spostarsi da un oggetto costante. I progettisti del linguaggio probabilmente l'hanno fatto per consentire agli elenchi di inizializzatori di contenere, ad esempio, costanti di stringa, da cui sarebbe inappropriato spostarsi.

Tuttavia, se ti trovi in ​​una situazione in cui sai che l'elenco degli inizializzatori contiene espressioni rvalue (o vuoi costringere 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 inizializzatore non siano Tvalori, ma valori che incapsulano T&&. Quindi, anche se questi valori stessi sono constqualificati, possono comunque recuperare un valore modificabile.

template<typename T>
  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 initializer_list<T>argomento, dichiari un initializer_list<rref_capture<T> >argomento. Ecco un esempio concreto, che coinvolge un vettore di std::unique_ptr<int>puntatori intelligenti, per il quale è definita solo la semantica di spostamento (quindi questi oggetti stessi non possono mai essere memorizzati in un elenco di inizializzatori); tuttavia l'elenco degli inizializzatori di seguito viene compilato senza problemi.

#include <memory>
#include <initializer_list>
class uptr_vec
{
  typedef std::unique_ptr<int> uptr; // move only type
  std::vector<uptr> data;
public:
  uptr_vec(uptr_vec&& v) : data(std::move(v.data)) {}
  uptr_vec(std::initializer_list<rref_capture<uptr> > 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<int> 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 degli inizializzatori dovessero essere veri prvalues ​​(nell'esempio sono xvalues), il linguaggio garantisce che la durata dei provvisori corrispondenti si estenda al punto in cui vengono utilizzati? Francamente, non credo che la sezione 8.5 pertinente dello standard affronti questo problema. Tuttavia, leggendo 1.9: 10, sembrerebbe che l' espressione completa pertinente in tutti i casi includa l'uso dell'elenco di inizializzatori, quindi penso che non ci sia pericolo di riferimenti rvalue penzolanti.


Costanti di stringa? Come "Hello world"? Se ti sposti da loro, copi semplicemente un puntatore (o associ un riferimento).
giorno

1
"Una domanda ha bisogno di una risposta" Gli inizializzatori all'interno {..}sono associati ai riferimenti nel parametro della funzione di rref_capture. Ciò non prolunga la loro vita, sono comunque distrutti alla fine della piena espressione in cui sono stati creati.
giorno

Secondo il commento di TC da un'altra risposta: se hai più sovraccarichi del costruttore, racchiudi il std::initializer_list<rref_capture<T>>tratto di trasformazione di tua scelta - diciamo, std::decay_t- per bloccare la deduzione indesiderata.
Ripristina Monica il

2

Ho pensato che potesse essere istruttivo offrire un punto di partenza ragionevole per una soluzione alternativa.

Commenti in linea.

#include <memory>
#include <vector>
#include <array>
#include <type_traits>
#include <algorithm>
#include <iterator>

template<class Array> struct maker;

// a maker which makes a std::vector
template<class T, class A>
struct maker<std::vector<T, A>>
{
  using result_type = std::vector<T, A>;

  template<class...Ts>
  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>(ts)),0)...
    });

    return result;
  }
};

// a maker which makes std::array
template<class T, std::size_t N>
struct maker<std::array<T, N>>
{
  using result_type = std::array<T, N>;

  template<class...Ts>
  auto operator()(Ts&&...ts) const
  {
    return result_type { std::forward<Ts>(ts)... };
  }

};

//
// delegation function which selects the correct maker
//
template<class Array, class...Ts>
auto make(Ts&&...ts)
{
  auto m = maker<Array>();
  return m(std::forward<Ts>(ts)...);
}

// vectors and arrays of non-copyable types
using vt = std::vector<std::unique_ptr<int>>;
using at = std::array<std::unique_ptr<int>,2>;


int main(){
    // build an array, using make<> for consistency
    auto a = make<at>(std::make_unique<int>(10), std::make_unique<int>(20));

    // build a vector, using make<> because an initializer_list requires a copyable type  
    auto v = make<vt>(std::make_unique<int>(10), std::make_unique<int>(20));
}

La domanda era se un initializer_listpuò essere spostato da, non se qualcuno avesse soluzioni alternative. Inoltre, il principale punto di forza di initializer_listè che è basato solo sul tipo di elemento, non sul numero di elementi, e quindi non richiede che anche i destinatari siano modellati - e questo lo perde completamente.
underscore_d

1
@underscore_d hai assolutamente ragione. Ritengo che la condivisione delle conoscenze relative alla domanda sia di per sé una buona cosa. In questo caso, forse ha aiutato l'OP e forse no - non ha risposto. Il più delle volte, tuttavia, il PO e altri accolgono con favore materiale aggiuntivo relativo alla domanda.
Richard Hodges

Certo, può davvero aiutare i lettori che vogliono qualcosa di simile initializer_listma non sono soggetti a tutti i vincoli che lo rendono utile. :)
underscore_d

@underscore_d quale dei vincoli ho trascurato?
Richard Hodges

Tutto quello che voglio dire è che initializer_list(tramite la magia del compilatore) evita di dover modellare funzioni sul numero di elementi, qualcosa che è intrinsecamente richiesto dalle alternative basate su array e / o funzioni variadiche, vincolando così la gamma di casi in cui queste ultime sono utilizzabili. A quanto mi risulta, questo è proprio uno dei principali motivi per avere initializer_list, quindi sembrava degno di nota.
underscore_d

0

Sembra non consentito nello standard attuale come già risposto . Ecco un'altra soluzione alternativa per ottenere qualcosa di simile, definendo la funzione come variadica invece di prendere un elenco di inizializzatori.

#include <vector>
#include <utility>

// begin helper functions

template <typename T>
void add_to_vector(std::vector<T>* vec) {}

template <typename T, typename... Args>
void add_to_vector(std::vector<T>* vec, T&& car, Args&&... cdr) {
  vec->push_back(std::forward<T>(car));
  add_to_vector(vec, std::forward<Args>(cdr)...);
}

template <typename T, typename... Args>
std::vector<T> make_vector(Args&&... args) {
  std::vector<T> result;
  add_to_vector(&result, std::forward<Args>(args)...);
  return result;
}

// end helper functions

struct S {
  S(int) {}
  S(S&&) {}
};

void bar(S&& s) {}

template <typename T, typename... Args>
void foo(Args&&... args) {
  std::vector<T> args_vec = make_vector<T>(std::forward<Args>(args)...);
  for (auto& arg : args_vec) {
    bar(std::move(arg));
  }
}

int main() {
  foo<S>(S(1), S(2), S(3));
  return 0;
}

I modelli Variadic possono gestire i riferimenti ai valori r in modo appropriato, a differenza di initializer_list.

In questo codice di esempio, ho utilizzato una serie 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.


La domanda era se un initializer_listpuò essere spostato da, non se qualcuno avesse soluzioni alternative. Inoltre, il principale punto di forza di initializer_listè che è basato solo sul tipo di elemento, non sul numero di elementi, e quindi non richiede che anche i destinatari siano modellati - e questo lo perde completamente.
underscore_d

0

Ho un'implementazione molto più semplice che fa uso di una classe wrapper che funge da tag per contrassegnare l'intenzione di spostare gli elementi. Questo è un costo in fase di compilazione.

La classe wrapper è progettata per essere utilizzata nel modo in cui std::moveviene utilizzata, basta sostituirla std::movecon move_wrapper, ma questo richiede C ++ 17. Per le specifiche precedenti, puoi utilizzare un metodo di creazione aggiuntivo.

Dovrai scrivere metodi / costruttori di builder che accettano classi wrapper all'interno initializer_liste spostare gli elementi di conseguenza.

Se hai bisogno di copiare alcuni elementi invece di spostarli, costruisci una copia prima di passarla a initializer_list.

Il codice dovrebbe essere auto-documentato.

#include <iostream>
#include <vector>
#include <initializer_list>

using namespace std;

template <typename T>
struct move_wrapper {
    T && t;

    move_wrapper(T && t) : t(move(t)) { // since it's just a wrapper for rvalues
    }

    explicit move_wrapper(T & t) : t(move(t)) { // acts as std::move
    }
};

struct Foo {
    int x;

    Foo(int x) : x(x) {
        cout << "Foo(" << x << ")\n";
    }

    Foo(Foo const & other) : x(other.x) {
        cout << "copy Foo(" << x << ")\n";
    }

    Foo(Foo && other) : x(other.x) {
        cout << "move Foo(" << x << ")\n";
    }
};

template <typename T>
struct Vec {
    vector<T> v;

    Vec(initializer_list<T> il) : v(il) {
    }

    Vec(initializer_list<move_wrapper<T>> il) {
        v.reserve(il.size());
        for (move_wrapper<T> const & w : il) {
            v.emplace_back(move(w.t));
        }
    }
};

int main() {
    Foo x{1}; // Foo(1)
    Foo y{2}; // Foo(2)

    Vec<Foo> v{Foo{3}, move_wrapper(x), Foo{y}}; // I want y to be copied
    // Foo(3)
    // copy Foo(2)
    // move Foo(3)
    // move Foo(1)
    // move Foo(2)
}

0

Invece di usare a std::initializer_list<T>, puoi dichiarare il tuo argomento come riferimento al valore di un array:

template <typename T>
void bar(T &&value);

template <typename T, size_t N>
void foo(T (&&list)[N] ) {
   std::for_each(std::make_move_iterator(std::begin(list)),
                 std::make_move_iterator(std::end(list)),
                 &bar);
}

void baz() {
   foo({std::make_unique<int>(0), std::make_unique<int>(1)});
}

Vedi esempio utilizzando std::unique_ptr<int>: https://gcc.godbolt.org/z/2uNxv6


-1

Considera l' in<T>idioma descritto su cpptruths . 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.


4
Perché diavolo vorresti determinare la categoria di valore in fase di esecuzione quando il compilatore lo sa già?
fredoverflow

1
Per favore leggi il blog e lasciami un commento se non sei d'accordo o hai un'alternativa migliore. Anche se il compilatore conosce la categoria del valore, initializer_list non la conserva perché ha solo iteratori const. Quindi è necessario "catturare" la categoria del valore quando si costruisce l'inizializer_list e passarla in modo che la funzione possa utilizzarla a piacimento.
Sumant

5
Questa risposta è fondamentalmente inutile senza seguire il collegamento e le risposte SO dovrebbero essere utili senza seguire i collegamenti.
Yakk - Adam Nevraumont

1
@Sumant [copiando il mio commento da un identico post altrove] Questo enorme pasticcio fornisce effettivamente vantaggi misurabili per le prestazioni o l'utilizzo della memoria e, in tal caso, una quantità sufficientemente grande di tali vantaggi per compensare adeguatamente quanto sia terribile e il fatto che impiega circa un'ora per capire cosa sta cercando di fare? Ne dubito.
underscore_d
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.