initializer_list und Verschiebungssemantik

96

Darf ich Elemente aus einem verschieben 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?
    }
}

Schon seit std::intializer_list<T> besondere Aufmerksamkeit des Compilers erfordert und keine Wertesemantik wie bei normalen Containern der C ++ - Standardbibliothek aufweist, möchte ich lieber auf Nummer sicher gehen und fragen.

Fredoverflow
quelle
Die Kernsprache definiert , die das Objekt durch eine bezeichnet initializer_list<T>sind nicht -const. Wie initializer_list<int>bezieht sich auf intObjekte. Aber ich denke, das ist ein Defekt - es ist beabsichtigt, dass Compiler eine Liste statisch im Nur-Lese-Speicher zuordnen können.
Johannes Schaub - Litb

Antworten:

89

Nein, das wird nicht wie beabsichtigt funktionieren. Sie erhalten weiterhin Kopien. Ich bin ziemlich überrascht darüber, da ich gedacht hatte, dass initializer_listes eine Reihe von Provisorien gibt, bis sie movefertig sind.

beginund endfür die initializer_listRückgabe const T *ist das Ergebnis movein Ihrem Code T const &&- eine unveränderliche Wertreferenz. Ein solcher Ausdruck kann nicht sinnvoll verschoben werden. Es wird an einen Funktionsparameter vom Typ T const &gebunden, da rWerte an konstante Wertreferenzen gebunden sind und Sie weiterhin die Kopiersemantik sehen.

Wahrscheinlich liegt der Grund dafür darin, dass der Compiler das wählen kann initializer_list statisch initialisierte Konstante festlegen kann, aber es scheint sauberer zu sein, den Typ initializer_listoder const initializer_listnach Ermessen des Compilers festzulegen, sodass der Benutzer nicht weiß, ob er eine constoder eine veränderbare Konstante erwarten soll Ergebnis von beginund end. Aber das ist nur mein Bauchgefühl, wahrscheinlich gibt es einen guten Grund, warum ich falsch liege.

Update: Ich habe einen ISO-Vorschlag zur initializer_listUnterstützung von Nur-Verschieben-Typen geschrieben. Es ist nur ein erster Entwurf, und er ist noch nirgendwo implementiert, aber Sie können ihn zur weiteren Analyse des Problems sehen.

Kartoffelklatsche
quelle
11
Falls es nicht klar ist, bedeutet dies immer noch, dass die Verwendung std::movesicher, wenn nicht produktiv ist. (Außer Zugkonstrukteure T const&&.)
Luc Danton
Ich glaube nicht, dass Sie das ganze Argument entweder const std::initializer_list<T>oder nur std::initializer_list<T>auf eine Weise vorbringen könnten, die nicht oft Überraschungen hervorruft. Bedenken Sie, dass jedes Argument in dem initializer_listentweder constoder nicht sein kann und das im Kontext des Aufrufers bekannt ist, aber der Compiler muss nur eine Version des Codes im Kontext des Angerufenen generieren (dh foodarin weiß er nichts über die Argumente dass der Anrufer vorbeikommt)
David Rodríguez - dribeas
1
@ David: Guter Punkt, aber es wäre immer noch nützlich, wenn eine std::initializer_list &&Überladung etwas bewirkt , auch wenn auch eine Überlastung ohne Referenz erforderlich ist. Ich nehme an, es wäre noch verwirrender als die derzeitige Situation, die bereits schlecht ist.
Potatoswatter
1
@JBJansen Es kann nicht gehackt werden. Ich sehe nicht genau, was dieser Code für initializer_list leisten soll, aber als Benutzer haben Sie nicht die erforderlichen Berechtigungen, um daraus zu wechseln. Sicherer Code wird dies nicht tun.
Potatoswatter
1
@ Potatoswatter, später Kommentar, aber wie ist der Status des Vorschlags. Gibt es eine entfernte Chance, dass es in C ++ 20 kommt?
WhiZTiM
20
bar(std::move(*it));   // kosher?

Nicht so, wie Sie es beabsichtigen. Sie können ein constObjekt nicht verschieben . Und std::initializer_listbietet nur constZugriff auf seine Elemente. So die Art der itheißt const T *.

Ihr Anrufversuch std::move(*it)führt nur zu einem l-Wert. IE: eine Kopie.

std::initializer_listverweist auf statischen Speicher. Dafür ist die Klasse da. Sie können nicht bewegen aus statischen Speicher, da die Bewegung es ändert sich bringt. Sie können nur davon kopieren.

Nicol Bolas
quelle
Ein const xvalue ist immer noch ein xvalue und initializer_listverweist auf den Stack, falls dies erforderlich ist. (Wenn der Inhalt nicht konstant ist, ist es immer noch threadsicher.)
Potatoswatter
5
@Potatoswatter: Sie können sich nicht von einem konstanten Objekt entfernen. Das initializer_listObjekt selbst kann ein x-Wert sein, aber sein Inhalt (das tatsächliche Array von Werten, auf das es zeigt) ist es const, da diese Inhalte statische Werte sein können. Sie können sich einfach nicht vom Inhalt eines entfernen initializer_list.
Nicol Bolas
Siehe meine Antwort und ihre Diskussion. Er hat den dereferenzierten Iterator verschoben und einen constx-Wert erzeugt. movemag bedeutungslos sein, aber es ist legal und sogar möglich, einen Parameter zu deklarieren, der genau das akzeptiert. Wenn das Verschieben eines bestimmten Typs ein No-Op ist, funktioniert es möglicherweise sogar richtig.
Potatoswatter
1
@Potatoswatter: Der C ++ 11-Standard verwendet viel Sprache, um sicherzustellen, dass nicht temporäre Objekte nur dann verschoben werden, wenn Sie sie verwenden std::move. Dies stellt sicher, dass Sie anhand der Inspektion erkennen können, wann ein Verschiebungsvorgang ausgeführt wird, da dies sowohl die Quelle als auch das Ziel betrifft (Sie möchten nicht, dass dies implizit für benannte Objekte erfolgt). Aus diesem Grund ist der Code irreführend , wenn Sie ihn std::movean einem Ort verwenden, an dem keine Verschiebungsoperation ausgeführt wird (und bei einem constx-Wert keine tatsächliche Bewegung stattfindet). Ich denke, es ist ein Fehler std::move, auf einem constObjekt aufrufbar zu sein .
Nicol Bolas
1
Vielleicht, aber ich werde immer noch weniger Ausnahmen von den Regeln über die Möglichkeit irreführenden Codes machen. Genau deshalb habe ich mit "Nein" geantwortet, obwohl es legal ist, und das Ergebnis ist ein x-Wert, auch wenn er nur als konstanter Wert gebunden wird. Um ehrlich zu sein, hatte ich bereits einen kurzen Flirt mit const &&einer in Müll gesammelten Klasse mit verwalteten Zeigern, in der alles Relevante veränderlich war und das Verschieben die Zeigerverwaltung bewegte, aber den enthaltenen Wert nicht beeinflusste. Es gibt immer schwierige Randfälle: v).
Potatoswatter
2

Dies funktioniert nicht wie angegeben, da list.begin()Typ hatconst T * ist und Sie sich nicht von einem konstanten Objekt entfernen können. Die Sprachdesigner haben dies wahrscheinlich gemacht, damit Initialisierungslisten beispielsweise Zeichenfolgenkonstanten enthalten können, von denen es unangemessen wäre, sie zu verschieben.

Wenn Sie sich jedoch in einer Situation befinden, in der Sie wissen, dass die Initialisierungsliste rWert-Ausdrücke enthält (oder Sie den Benutzer zwingen möchten, diese zu schreiben), gibt es einen Trick, mit dem dies funktioniert (ich wurde von der Antwort von Sumant für inspiriert dies, aber die Lösung ist viel einfacher als diese). Die in der Initialisiererliste gespeicherten Elemente müssen keine TWerte sein, sondern Werte, die kapseln T&&. Selbst wenn diese Werte selbst constqualifiziert sind, können sie dennoch einen veränderbaren Wert abrufen.

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

Anstatt ein initializer_list<T>Argument zu deklarieren, deklarieren Sie jetzt ein initializer_list<rref_capture<T> >Argument. Hier ist ein konkretes Beispiel mit einem Vektor von std::unique_ptr<int>intelligenten Zeigern, für die nur die Bewegungssemantik definiert ist (daher können diese Objekte selbst niemals in einer Initialisierungsliste gespeichert werden). Die unten stehende Initialisierungsliste lässt sich jedoch problemlos kompilieren.

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

Eine Frage muss beantwortet werden: Wenn die Elemente der Initialisierungsliste echte Werte sein sollen (im Beispiel sind es x-Werte), stellt die Sprache dann sicher, dass sich die Lebensdauer der entsprechenden Provisorien bis zu dem Punkt erstreckt, an dem sie verwendet werden? Ehrlich gesagt glaube ich nicht, dass der relevante Abschnitt 8.5 des Standards dieses Problem überhaupt behandelt. Wenn man jedoch 1,9: 10 liest, scheint es, dass der relevante vollständige Ausdruck in allen Fällen die Verwendung der Initialisiererliste umfasst, so dass ich denke, dass keine Gefahr besteht, dass rvalue-Referenzen baumeln.

Marc van Leeuwen
quelle
String-Konstanten? Wie "Hello world"? Wenn Sie sich von ihnen entfernen, kopieren Sie einfach einen Zeiger (oder binden eine Referenz).
Dyp
1
"Eine Frage braucht eine Antwort" Die darin enthaltenen Initialisierer {..}sind an Referenzen im Funktionsparameter von gebunden rref_capture. Dies verlängert nicht ihre Lebensdauer, sie werden am Ende des vollständigen Ausdrucks, in dem sie erstellt wurden, immer noch zerstört.
Dyp
Per TCs Kommentar aus einer anderen Antwort: Wenn Sie mehrere Überladungen des Konstruktors haben, wickeln Sie das std::initializer_list<rref_capture<T>>in ein Transformationsmerkmal Ihrer Wahl ein, std::decay_tum beispielsweise unerwünschte Abzüge zu blockieren.
Stellen Sie Monica
2

Ich dachte, es könnte lehrreich sein, einen vernünftigen Ausgangspunkt für eine Problemumgehung anzubieten.

Kommentare inline.

#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));
}
Richard Hodges
quelle
Die Frage war, ob eine initializer_listverschoben werden kann und nicht, ob jemand Problemumgehungen hatte. Außerdem ist das Hauptverkaufsargument von, initializer_listdass es nur für den Elementtyp und nicht für die Anzahl der Elemente verwendet wird und daher keine Empfänger erforderlich sind - und dies verliert dies vollständig.
underscore_d
1
@underscore_d du hast absolut recht. Ich bin der Ansicht, dass der Austausch von Wissen im Zusammenhang mit der Frage an sich schon eine gute Sache ist. In diesem Fall hat es vielleicht dem OP geholfen und vielleicht auch nicht - er hat nicht geantwortet. Meistens begrüßen das OP und andere jedoch zusätzliches Material im Zusammenhang mit der Frage.
Richard Hodges
Sicher, es kann in der Tat für Leser hilfreich sein, die so etwas wollen, initializer_listaber nicht allen Einschränkungen unterliegen, die es nützlich machen. :)
underscore_d
@underscore_d Welche der Einschränkungen habe ich übersehen?
Richard Hodges
Ich meine nur, dass initializer_list(über Compiler Magic) vermieden wird, dass Funktionen für die Anzahl der Elemente vorlagen müssen, was von Alternativen, die auf Arrays und / oder variadischen Funktionen basieren, von Natur aus erforderlich ist, wodurch der Bereich der Fälle eingeschränkt wird, in denen letztere verwendbar sind. Nach meinem Verständnis ist dies genau eine der wichtigsten Gründe dafür initializer_list, weshalb es erwähnenswert schien.
underscore_d
0

Es scheint im aktuellen Standard nicht erlaubt zu sein, wie bereits beantwortet . Hier ist eine weitere Problemumgehung, um etwas Ähnliches zu erreichen, indem Sie die Funktion als variadisch definieren, anstatt eine Initialisierungsliste zu erstellen.

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

Variadic-Vorlagen können im Gegensatz zu initializer_list R-Wert-Referenzen angemessen verarbeiten.

In diesem Beispielcode habe ich eine Reihe kleiner Hilfsfunktionen verwendet, um die variadischen Argumente in einen Vektor zu konvertieren und sie dem ursprünglichen Code ähnlich zu machen. Natürlich können Sie stattdessen direkt eine rekursive Funktion mit variablen Vorlagen schreiben.

Hiroshi Ichikawa
quelle
Die Frage war, ob eine initializer_listverschoben werden kann und nicht, ob jemand Problemumgehungen hatte. Außerdem ist das Hauptverkaufsargument von, initializer_listdass es nur für den Elementtyp und nicht für die Anzahl der Elemente verwendet wird und daher keine Empfänger erforderlich sind - und dies verliert dies vollständig.
underscore_d
0

Ich habe eine viel einfachere Implementierung, die eine Wrapper-Klasse verwendet, die als Tag fungiert, um die Absicht zu markieren, die Elemente zu verschieben. Dies sind Kosten für die Kompilierung.

Die Wrapper - Klasse ist so konzipiert , in der Art und Weise verwendet werden , std::moveverwendet wird, ersetzen Sie einfach std::movemit move_wrapper, aber dies erfordert C ++ 17. Für ältere Spezifikationen können Sie eine zusätzliche Builder-Methode verwenden.

Sie müssen Builder-Methoden / Konstruktoren schreiben, die Wrapper-Klassen akzeptieren, initializer_listund die Elemente entsprechend verschieben.

Wenn Sie möchten, dass einige Elemente kopiert werden, anstatt verschoben zu werden, erstellen Sie eine Kopie, bevor Sie sie an übergeben initializer_list.

Der Code sollte selbst dokumentiert sein.

#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)
}
bumfo
quelle
0

Anstatt a zu verwenden std::initializer_list<T>, können Sie Ihr Argument als Array-Wertreferenz deklarieren:

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

Siehe Beispiel unter Verwendung von std::unique_ptr<int>: https://gcc.godbolt.org/z/2uNxv6

Jorge Bellon
quelle
-1

Betrachten Sie die in<T>auf cpptruths beschriebene Redewendung . Die Idee ist, lvalue / rvalue zur Laufzeit zu bestimmen und dann move oder copy-building aufzurufen. in<T>erkennt rvalue / lvalue, obwohl die von initializer_list bereitgestellte Standardschnittstelle eine konstante Referenz ist.

Sumant
quelle
4
Warum um alles in der Welt möchten Sie die Wertekategorie zur Laufzeit bestimmen, wenn der Compiler sie bereits kennt?
Fredoverflow
1
Bitte lesen Sie den Blog und hinterlassen Sie mir einen Kommentar, wenn Sie nicht einverstanden sind oder eine bessere Alternative haben. Selbst wenn der Compiler die Wertekategorie kennt, behält initializer_list sie nicht bei, da sie nur konstante Iteratoren enthält. Sie müssen also die Wertekategorie "erfassen", wenn Sie die initializer_list erstellen und durchlaufen, damit die Funktion sie nach Belieben verwenden kann.
Sumant
5
Diese Antwort ist grundsätzlich nutzlos, ohne dem Link zu folgen, und SO-Antworten sollten nützlich sein, ohne den Links zu folgen.
Yakk - Adam Nevraumont
1
@Sumant [Kopieren meines Kommentars aus einem identischen Beitrag an anderer Stelle] Bietet dieses gewaltige Durcheinander tatsächlich messbare Vorteile für die Leistung oder die Speichernutzung, und wenn ja, eine ausreichend große Menge solcher Vorteile, um das schreckliche Aussehen und die Tatsache, dass es aussieht, angemessen auszugleichen dauert es ungefähr eine Stunde, um herauszufinden, was es versucht zu tun? Ich bezweifle es irgendwie.
underscore_d