Comment fonctionne std :: tie?

120

Je l'ai utilisé std::tiesans trop y réfléchir. Cela fonctionne donc je viens d'accepter cela:

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
}

Mais comment fonctionne cette magie noire ? Comment un temporaire créé par le std::tiechangement aet b? Je trouve cela plus intéressant car c'est une fonctionnalité de bibliothèque, pas une fonctionnalité de langage, donc c'est certainement quelque chose que nous pouvons implémenter nous-mêmes et comprendre.

bolov
la source

Réponses:

152

Afin de clarifier le concept de base, réduisons-le à un exemple plus basique. Bien que cela std::tiesoit utile pour les fonctions retournant (un tuple de) plus de valeurs, nous pouvons le comprendre très bien avec une seule valeur:

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

Ce que nous devons savoir pour aller de l'avant:

  • std::tie construit et renvoie un tuple de références.
  • std::tuple<int>et std::tuple<int&>sont 2 classes complètement différentes, sans lien entre eux, d' autres qu'ils ont été générés à partir du même modèle, std::tuple.
  • tuple a un tuple operator=acceptant un tuple de types différents (mais le même nombre), où chaque membre est assigné individuellement - de cppreference :

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

    (3) Pour tout i, assigne std::get<i>(other)à std::get<i>(*this).

La prochaine étape consiste à se débarrasser de ces fonctions qui ne font que vous gêner, afin que nous puissions transformer notre code en ceci:

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

L'étape suivante consiste à voir exactement ce qui se passe à l'intérieur de ces structures. Pour cela, je crée 2 types de Tsubstituant pour std::tuple<int>et Trsubstituant std::tuple<int&>, dépouillés au strict minimum pour nos opérations:

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
}

Et enfin, j'aime me débarrasser des structures toutes ensemble (enfin, ce n'est pas 100% équivalent, mais c'est assez proche pour nous, et assez explicite pour le permettre):

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
}

Donc, fondamentalement, std::tie(a)initialise une référence de membre de données à a. std::tuple<int>(24)crée un membre de données avec une valeur 24et l'affectation affecte 24 à la référence de membre de données dans la première structure. Mais depuis que membre de données est une référence liée à a, que , fondamentalement , cède 24à a.

bolov
la source
1
Ce qui me dérange, c'est que nous appelons l'opérateur d'affectation à une rvalue.
Adam Zahran
Dans cette réponse, il a déclaré qu'un conteneur ne peut pas contenir de référence. Pourquoi tuplepourrait détenir une référence?
nn0p
6
@ nn0p std::tuplen'est pas un conteneur, du moins pas dans la terminologie C ++, pas le même que le std::vectoret les goûts. Par exemple, vous ne pouvez pas itérer de la manière habituelle sur un tuple car il contient différents types d'objets.
bolov
@Adam tie (x, y) = make_pair (1,2); devient en fait std :: tie (x, y) .operator = (std :: make_pair (1, 2)), c'est pourquoi "l'assignation à une rvalue" fonctionne XD
Ju Piece
30

Cela ne répond en aucun cas à votre question, mais permettez-moi de le poster quand même car C ++ 17 est fondamentalement prêt (avec le support du compilateur), donc tout en vous demandant comment fonctionne le matériel obsolète, il vaut probablement la peine de regarder comment l'actuel, et future, la version de C ++ fonctionne aussi.

Avec C ++ 17, vous pouvez à peu près gratter std::tieen faveur de ce que l'on appelle les liaisons structurées . Ils font la même chose (enfin, pas la même chose , mais ils ont le même effet net), bien que vous ayez besoin de taper moins de caractères, cela n'a pas besoin de support de bibliothèque, et vous avez également la possibilité de prendre des références, si cela se trouve être ce que tu veux.

(Notez qu'en C ++ 17, les constructeurs effectuent la déduction d'arguments, ce qui make_tupleest devenu quelque peu superflu également.)

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
la source
2
Si cette dernière ligne se compile, je suis un peu inquiet. Cela ressemble à la liaison d'une référence à un temporaire, ce qui est illégal.
Nir Friedman
3
@Neil Il doit s'agir soit d'une référence rvalue, soit d'une référence const lvalue. Vous ne pouvez pas lier une référence lvalue à une prvalue (temporaire). Bien que cela ait été une "extension" de MSVC depuis des lustres.
Nir Friedman
1
Il est probablement également intéressant de mentionner que contrairement tieaux liaisons structurées, les liaisons structurées peuvent être utilisées de cette manière sur des types qui ne sont pas constructibles par défaut.
Dan
5
Oui, std::tie()c'est beaucoup moins utile depuis C ++ 17, où les liaisons structurées sont généralement supérieures, mais il a toujours des utilisations, y compris l'attribution à des variables existantes (pas simultanément nouvellement déclarées) et de faire de manière concise d'autres choses comme échanger plusieurs variables ou d'autres choses qui doit attribuer des références.
underscore_d