¿Cómo funciona std :: tie?

120

Lo he usado std::tiesin pensarlo mucho. Funciona, así que acabo de aceptar que:

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
}

Pero, ¿cómo funciona esta magia negra ? ¿Cómo se crea un temporal por std::tiecambio ay b? Encuentro esto más interesante ya que es una característica de la biblioteca, no una característica del lenguaje, así que seguramente es algo que podemos implementar y entender por nosotros mismos.

bolov
fuente

Respuestas:

152

Para aclarar el concepto central, reduzcamoslo a un ejemplo más básico. Aunque std::tiees útil para funciones que devuelven (una tupla de) más valores, podemos entenderlo bien con un solo valor:

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

Cosas que debemos saber para seguir adelante:

  • std::tie construye y devuelve una tupla de referencias.
  • std::tuple<int>y std::tuple<int&>2 clases completamente diferentes, sin conexión entre ellos, otros que se generaron a partir de la misma plantilla, std::tuple.
  • tuple tiene una tupla que operator=acepta una tupla de diferentes tipos (pero el mismo número), donde cada miembro se asigna individualmente, de cppreference :

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

    (3) Para todo i, asigna std::get<i>(other)a std::get<i>(*this).

El siguiente paso es deshacerse de esas funciones que solo se interponen en su camino, para que podamos transformar nuestro código a esto:

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

El siguiente paso es ver exactamente qué sucede dentro de esas estructuras. Para esto, creo 2 tipos de Tsustituyentes std::tuple<int>y Trsustituyentes std::tuple<int&>, reducidos al mínimo para nuestras operaciones:

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
}

Y finalmente, me gusta deshacerme de las estructuras todas juntas (bueno, no es 100% equivalente, pero es lo suficientemente cercano para nosotros y lo suficientemente explícito como para permitirlo):

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
}

Básicamente, std::tie(a)inicializa una referencia de miembro de datos a a. std::tuple<int>(24)crea un miembro de datos con valor 24, y la asignación asigna 24 a la referencia del miembro de datos en la primera estructura. Pero dado que ese miembro de datos es una referencia vinculada a a, básicamente se asigna 24a a.

bolov
fuente
1
Lo que me molesta es que estamos llamando al operador de asignación a un rvalue.
Adam Zahran
En esta respuesta, indicó que un contenedor no puede contener una referencia. ¿Por qué tuplepodría tener una referencia?
nn0p
6
@ nn0p std::tupleno es un contenedor, al menos no en la terminología de C ++, no es lo mismo que el std::vectory similares. Por ejemplo, no puede iterar con las formas habituales sobre una tupla porque contiene diferentes tipos de objetos.
bolov
@Adam tie (x, y) = make_pair (1,2); en realidad se convierte en std :: tie (x, y) .operator = (std :: make_pair (1, 2)), es por eso que la "asignación a un valor r" funciona XD
Ju Piece
30

Esto no responde a su pregunta de ninguna manera, pero déjeme publicarlo de todos modos porque C ++ 17 está básicamente listo (con soporte para el compilador), por lo que mientras se pregunta cómo funcionan las cosas desactualizadas, probablemente valga la pena ver cómo funciona el actual, y futura, la versión de C ++ también funciona.

Con C ++ 17 se puede hacer un scratch std::tiea favor de lo que se llama enlaces estructurados . Hacen lo mismo (bueno, no lo mismo , pero tienen el mismo efecto neto), aunque necesita escribir menos caracteres, no necesita soporte de biblioteca, y también tiene la capacidad de tomar referencias, si eso sucede. Lo que quieras.

(Tenga en cuenta que en C ++ 17 los constructores hacen deducción de argumentos, por lo que también se make_tupleha vuelto algo superfluo).

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
fuente
2
Si esa última línea se compila, estoy un poco preocupado. Parece vincular una referencia a un temporal que es ilegal.
Nir Friedman
3
@Neil Tiene que ser una referencia rvalue o una referencia constante lvalue. No puede vincular una referencia lvalue a un prvalue (temporal). Aunque esto ha sido una "extensión" en MSVC durante años.
Nir Friedman
1
Probablemente también valga la pena mencionar que tie, a diferencia de los enlaces estructurados, se pueden usar de esta manera en tipos que no se pueden construir por defecto.
Dan
5
Sí, std::tie()es mucho menos útil desde C ++ 17, donde los enlaces estructurados suelen ser superiores, pero todavía tiene usos, incluida la asignación a variables existentes (no declaradas simultáneamente recientemente) y hacer de manera concisa otras cosas como intercambiar múltiples variables u otras cosas que debe asignar a referencias.
underscore_d