Como funciona o std :: tie?

120

Eu usei std::tiesem pensar muito nisso. Funciona, então acabei de aceitar 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
}

Mas como funciona essa magia negra ? Como um temporário criado pela std::tiemudança ae b? Acho isso mais interessante, pois é um recurso de biblioteca, não um recurso de linguagem, então certamente é algo que podemos implementar e entender.

Bolov
fonte

Respostas:

152

Para esclarecer o conceito central, vamos reduzi-lo a um exemplo mais básico. Embora std::tieseja útil para funções que retornam (uma tupla de) mais valores, podemos entendê-lo perfeitamente com apenas um valor:

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

Coisas que precisamos saber para seguir em frente:

  • std::tie constrói e retorna uma tupla de referências.
  • std::tuple<int>e std::tuple<int&>são 2 classes completamente diferentes, sem nenhuma conexão entre elas, outras que foram geradas a partir do mesmo template std::tuple,.
  • tupla operator=aceita uma tupla de diferentes tipos (mas mesmo número), onde cada membro é atribuído individualmente - de cppreference :

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

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

A próxima etapa é se livrar dessas funções que apenas atrapalham seu caminho, para que possamos transformar nosso código para o seguinte:

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

A próxima etapa é ver exatamente o que acontece dentro dessas estruturas. Para isso, crio 2 tipos de Tsubstituintes std::tuple<int>e Trsubstituintes std::tuple<int&>, reduzidos ao mínimo para nossas operações:

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
}

E, finalmente, gosto de me livrar das estruturas todas juntas (bem, não é 100% equivalente, mas é próximo o suficiente para nós e explícito o suficiente para permitir):

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
}

Então, basicamente, std::tie(a)inicializa uma referência de membro de dados para a. std::tuple<int>(24)cria um membro de dados com valor 24e a atribuição atribui 24 à referência do membro de dados na primeira estrutura. Mas, como esse membro de dados é um limite de referência a, basicamente atribui 24a a.

Bolov
fonte
1
O que me incomoda é que estamos chamando o operador de atribuição para um rvalue.
Adam Zahran,
Em esta resposta, ele afirmou que um recipiente não pode segurar uma referência. Por que tuplepoderia conter uma referência?
nn0p
6
@ nn0p std::tuplenão é um contêiner, pelo menos não na terminologia C ++, não é o mesmo que the std::vectore similares. Por exemplo, você não pode iterar com as formas usuais em uma tupla porque ela contém diferentes tipos de objetos.
bolov
@Adam tie (x, y) = make_pair (1,2); na verdade torna-se std :: tie (x, y) .operator = (std :: make_pair (1, 2)), é por isso que "atribuição a um rvalue" funciona XD
Ju Piece
30

Isso não responde a sua pergunta de forma alguma, mas deixe-me postá-la de qualquer maneira porque C ++ 17 está basicamente pronto (com suporte de compilador), então, enquanto você se pergunta como funciona o material desatualizado, provavelmente vale a pena olhar como o atual, e futuro, a versão do C ++ também funciona.

Com o C ++ 17, você pode praticamente ignorar o std::tieque é chamado de ligações estruturadas . Eles fazem o mesmo (bem, não o mesmo , mas eles têm o mesmo efeito), embora você precise digitar menos caracteres, não precisa do suporte da biblioteca e você também tem a capacidade de obter referências, se isso acontecer o que você quer.

(Observe que em C ++ 17 os construtores fazem dedução de argumento, então make_tuplese tornou um tanto supérfluo também.)

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
fonte
2
Se essa última linha compilar, fico um pouco preocupado. Parece vincular uma referência a um temporário que é ilegal.
Nir Friedman
3
@Neil Tem que ser uma referência rvalue ou uma referência const lvalue. Você não pode vincular uma referência lvalue a um prvalue (temporário). Embora isso tenha sido uma "extensão" do MSVC por muito tempo.
Nir Friedman
1
Provavelmente também vale a pena mencionar que tie, ao contrário , as ligações estruturadas podem ser usadas dessa forma em tipos que não podem ser construídos por padrão.
Dan
5
Sim, std::tie()é muito menos útil desde C ++ 17, onde ligações estruturadas são geralmente superiores, mas ainda tem usos, incluindo a atribuição de variáveis ​​existentes (não declaradas simultaneamente) e concisamente fazer outras coisas como trocar várias variáveis ​​ou outras coisas que deve atribuir a referências.
underscore_d