Wie funktioniert std :: tie?

120

Ich habe verwendet, std::tieohne viel darüber nachzudenken. Es funktioniert, also habe ich das einfach akzeptiert:

auto test()
{
   int a, b;
   std::tie(a, b) = std::make_tuple(2, 3);
   // a is now 2, b is now 3
   return a + b; // 5
}

Aber wie funktioniert diese schwarze Magie ? Wie entsteht eine temporäre durch std::tieVeränderung aund b? Ich finde das interessanter, da es sich um eine Bibliotheksfunktion handelt, nicht um eine Sprachfunktion. Daher können wir sie sicherlich selbst implementieren und verstehen.

Bolov
quelle

Antworten:

152

Um das Kernkonzept zu verdeutlichen, reduzieren wir es auf ein grundlegenderes Beispiel. Obwohl std::tiedies für Funktionen nützlich ist, die (ein Tupel) mehr Werte zurückgeben, können wir es mit nur einem Wert gut verstehen:

int a;
std::tie(a) = std::make_tuple(24);
return a; // 24

Dinge, die wir wissen müssen, um vorwärts zu kommen:

  • std::tie erstellt und gibt ein Tupel von Referenzen zurück.
  • std::tuple<int>und std::tuple<int&>sind 2 völlig verschiedene Klassen, ohne Verbindung zwischen ihnen, außer dass sie aus derselben Vorlage generiert wurden std::tuple.
  • Tupel hat ein operator=akzeptierendes Tupel verschiedener Typen (aber derselben Nummer), wobei jedes Mitglied einzeln zugewiesen wird - von cppreference :

    template< class... UTypes >
    tuple& operator=( const tuple<UTypes...>& other );
    

    (3) Weist für alle i std::get<i>(other)zu std::get<i>(*this).

Der nächste Schritt besteht darin, die Funktionen zu entfernen, die Ihnen nur im Weg stehen, damit wir unseren Code folgendermaßen umwandeln können:

int a;
std::tuple<int&>{a} = std::tuple<int>{24};
return a; // 24

Der nächste Schritt besteht darin, genau zu sehen, was in diesen Strukturen passiert. Dafür erstelle ich zwei Arten von TSubstituenten für std::tuple<int>und TrSubstituenten std::tuple<int&>, die für unsere Operationen auf das Nötigste reduziert sind:

struct T { // substituent for std::tuple<int>
    int x;
};

struct Tr { // substituent for std::tuple<int&>
    int& xr;

    auto operator=(const T& other)
    {
       // std::get<I>(*this) = std::get<I>(other);
       xr = other.x;
    }
};

auto foo()
{
    int a;
    Tr{a} = T{24};

    return a; // 24
}

Und schließlich möchte ich die Strukturen alle zusammen loswerden (nun, es ist nicht 100% äquivalent, aber es ist nah genug für uns und explizit genug, um es zuzulassen):

auto foo()
{
    int a;

    { // block substituent for temporary variables

    // Tr{a}
    int& tr_xr = a;

    // T{24}
    int t_x = 24;

    // = (asignement)
    tr_xr = t_x;
    }

    return a; // 24
}

std::tie(a)Initialisiert also im Grunde einen Datenelementverweis auf a. std::tuple<int>(24)Erstellt ein Datenelement mit Wert 24, und die Zuweisung weist der Datenelementreferenz in der ersten Struktur 24 zu. Aber da dieses Datenelement eine Referenz gebunden a, dass im Grunde Abtretungsempfänger 24zu a.

Bolov
quelle
1
Was mich stört, ist, dass wir den Zuweisungsoperator zu einem r-Wert aufrufen.
Adam Zahran
In dieser Antwort wurde angegeben, dass ein Container keine Referenz enthalten kann. Warum tuplekönnte eine Referenz enthalten sein?
nn0p
6
@ nn0p std::tupleist kein Container, zumindest nicht in der C ++ - Terminologie, nicht dasselbe wie das std::vectorund dergleichen. Zum Beispiel können Sie nicht mit den üblichen Methoden über ein Tupel iterieren, da es verschiedene Arten von Objekten enthält.
Bolov
@Adam tie (x, y) = make_pair (1,2); wird tatsächlich zu std :: tie (x, y) .operator = (std :: make_pair (1, 2)), deshalb funktioniert "Zuordnung zu einem Wert" XD
Ju Piece
30

Dies beantwortet Ihre Frage in keiner Weise, aber lassen Sie mich sie trotzdem posten, da C ++ 17 im Grunde genommen bereit ist (mit Compiler-Unterstützung). Wenn Sie sich also fragen, wie das veraltete Material funktioniert, lohnt es sich wahrscheinlich, einen Blick darauf zu werfen, wie das aktuelle und Zukünftig funktioniert auch die Version von C ++.

Mit C ++ 17 können Sie so ziemlich std::tiezugunsten sogenannter strukturierter Bindungen kratzen . Sie tun dasselbe (naja, nicht dasselbe , aber sie haben den gleichen Nettoeffekt), obwohl Sie weniger Zeichen eingeben müssen, keine Bibliotheksunterstützung benötigen und Sie auch die Möglichkeit haben, Referenzen zu übernehmen, wenn dies der Fall ist was du willst

(Beachten Sie, dass Konstruktoren in C ++ 17 Argumente ableiten und daher make_tupleauch etwas überflüssig geworden sind.)

int a, b;
std::tie(a, b) = std::make_tuple(2, 3);

// C++17
auto  [c, d] = std::make_tuple(4, 5);
auto  [e, f] = std::tuple(6, 7);
std::tuple t(8,9); auto& [g, h] = t; // not possible with std::tie
Damon
quelle
2
Wenn diese letzte Zeile kompiliert wird, bin ich etwas besorgt. Es sieht so aus, als würde man einen Verweis auf ein temporäres Objekt binden, das illegal ist.
Nir Friedman
3
@Neil Es muss entweder eine r-Wert-Referenz oder eine konstante Wert-Referenz sein. Sie können eine lvalue-Referenz nicht an einen prvalue (temporär) binden. Obwohl dies seit Ewigkeiten eine "Erweiterung" in MSVC ist.
Nir Friedman
1
Erwähnenswert ist wahrscheinlich auch, dass tiestrukturierte Bindungen im Gegensatz zu strukturierten Bindungen auf diese Weise für Typen verwendet werden können, die nicht standardmäßig konstruierbar sind.
Dan
5
Ja, es std::tie()ist viel weniger nützlich seit C ++ 17, wo strukturierte Bindungen normalerweise überlegen sind, aber es hat immer noch Verwendungsmöglichkeiten, einschließlich der Zuweisung zu vorhandenen (nicht gleichzeitig neu deklarierten) Variablen und der prägnanten Ausführung anderer Dinge wie dem Austausch mehrerer Variablen oder anderer Dinge, die muss Referenzen zuordnen.
underscore_d