O que é tupla em C++? A irmã mais velha do pair

Tupla em C++, a irmã mais velha do pair. Cobra python cancelada para indicar que é a tupla do C++, e não do python.

Você já tentou alguma vez criar uma variável que guardasse objetos de vários tipos diferentes e percebeu que isso era impossível em C++? Felizmente, no C++11 um novo tipo foi adicionado à linguagem: a tupla.

A tupla em C++ (std::tuple) é uma coleção de tamanho fixo cujos valores são de tipos possivelmente diferentes. Ela é uma generalização do par (std::pair).

Sintaxe – std::tuple

std::tuple<tipo1, tipo2, tipo3, …> nomeVar {valor1, valor 2, valor3, …}

Vejamos a seguir um exemplo de como se criar uma tupla e como acessar seus elementos.


Exemplo 1 – Como criar uma tupla em C++

#include <tuple>
#include <iostream>

using namespace std;

int main()
{
    tuple<int, bool, string> t1 {7, false, "Beleza?"};
    // CTAD - Class template argument deduction
    tuple t2 {8, true, "CTAD"s};
    cout << "t1 = (" << get<0>(t1) << ", " << get<1>(t1) << ", " << get<2>(t1) << ")" << endl;
    cout << "t2 = (" << get<0>(t2) << ", " << get<1>(t2) << ", " << get<2>(t2) << ")" << endl;
}

t1 = (7, 0, Beleza?)
t2 = (8, 1, CTAD)

No exemplo 1 vemos duas formas de se criar uma tupla. A primeira utiliza o std::tuple e especifica os tipos dos elementos que serão armazenados no objeto (linha 8). A segunda também utiliza o std::tuple, mas não especifica os tipos dos objetos armazenados em t2 – os tipos são deduzidos automaticamente através de um mecanismo chamado Class Template Argument Deduction (CTAD), ou dedução de argumento de classe template, em português.

O CTAD é um mecanismo pelo qual o compilador é capaz de deduzir os tipos dos argumentos utilizados para se incializar um objeto de uma classe template (para entender melhor os templates, veja nosso artigo sobre funções template).

No fim do exemplo, utilizei a função std::get para obter o valor armazenado em cada uma das posições das duas tuplas. A função std::get recebe um único argumento de template, o índice do elemento que se deseja obter, e um único argumento “normal” (dentro do parêntesis) que é a tupla em questão.


Como obter o tipo dos elementos de uma tupla em C++?

Nesta altura, você pode ter se perguntado se é possível descobrir o tipo dos elementos de uma tupla em C++, e a resposta é sim. A notação necessária para fazê-lo é um pouco longa, mas não há nada de misterioso nela. Vamos ao exemplo.

Exemplo 2 – Obter tipo de um elemento da tupla em C++

#include <tuple>
#include <iostream>

using namespace std;

int main()
{
    tuple<int, bool, string> t1 {7, false, "Beleza?"};
    
    cout << "Tipo do primeiro elemento de t1: " << typeid(get<0>(t1)).name() << endl;
    cout << "Tipo do segundo elemento de t1: " << typeid(tuple_element<1, decltype(t1)>::type).name() << endl;
    cout << "Tipo do terceiro elemento de t1: " << typeid(tuple_element<2, decltype(t1)>::type).name() << endl;
}

Tipo do primeiro elemento de t1: i
Tipo do segundo elemento de t1: b
Tipo do terceiro elemento de t1: NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE

Como vemos acima, para se obter o tipo de um elemento de uma tupla começamos por utilizar o operador typeid, que permite obter informações sobre quaisquer tipos em C++. O typeid recebe como argumento um tipo de objeto ou uma expressão (a partir da qual ele deduz o tipo e obtem-lhe informações).

Em seguida, há duas opções

  1. Passar o próprio elemento da tupla como argumento do typeid, como feito na linha 10;
  2. Passar o tipo do elemento da tupla usando a classe template tuple_element.

Para utilizar a opção 2, é preciso preencher os dois parâmetros template (que vão dentro do <>) do tuple_element. O primeiro deles é o índice do elemento da tupla que nos interessa, e o segundo é o tipo da tupla (e não a própria tupla), que pode ser escrito explicitamente como tuple<int, bool, string>, ou pode ser obtido usando o especificador decltype, como em decltype(t1).

Por fim, basta utilizar o método name() no retorno do operador typeid para se obter a forma textual do tipo do elemento. Note que para strings, o tipo retornado depende do compilador.


Como descobrir o tamanho de uma tupla em C++?

O tamanho de uma tupla pode ser encontrado utilizando a classe auxiliar chamada std::tuple_size (definido no header <tuple>), e basta-lhe passar como argumento o tipo da tupla. Simples, não?

Ainda por cima, o tuple_size permite encontrar o tamanho da tupla durante a compilação do programa (antes mesmo da sua execução).

Vejamos um exemplo a seguir.

Exemplo 3 – Tamanho de uma tupla em C++

#include <tuple>
#include <iostream>

using namespace std;

int main()
{
    tuple<char, double, string> t1 {'a', 10.5, "Beleza?"};
    
    cout << "Tamanho da tupla t1: " << tuple_size<decltype(t1)>::value << endl;
    cout << "Tamanho da tupla t1: " << tuple_size<tuple<char, double, string>>::value << endl;
}

Tamanho da tupla t1: 3
Tamanho da tupla t1: 3


Make_tuple e como armazenar referências em uma tupla

Outra forma de se criar tuplas em C++ é utilizar a função std::make_tuple, que recebe como argumentos de template os tipos dos elementos que se desejar armazenar na tupla, e como argumentos da função os próprios valores a guardar.

Todavia, para criar t1 no exemplo 4, notem que não informei os tipos dos elementos da tupla para o make_tuple. Como pode isso? Bom, isso é permitido porque, assim como existe o CTAD para as classes template, também existe o FTAD (Function Template Argument Deduction, ou Dedução de Argumento de Função Template) que detecta automaticamente os tipos dos argumentos passados para uma função template. Massa, não?

Referências em uma tupla

Para armazenar elementos do tipo referência em uma tupla em C++, basta utilizar o tipo dos elementos como referências (para relembrar como funcionam as referências em C++, revisite nosso artigo sobre o assunto), como tuple<std::string&, bool> t1, por exemplo.

No entanto, se você quiser usar a dedução automática de tipos, não é possível informar ao compilador que o tipo do elemento a ser armazenado na tupla é uma referência escrevendo & depois do tipo (afinal de contas, os tipos são omitidos). Mas calma, nem tudo está perdido! é possível utilizar as funções std::ref e std::cref, do header <functional>, para transformar os seus valores em referências e referências constantes, respectivamente.

Vejamos o uso dessas duas funções no exemplo a seguir.

Exemplo 4 – Make_tuple e referências em uma tupla em C++

#include <tuple>
#include <iostream>
#include <functional>

using namespace std;

int main()
{
    int i {10};
    const string s {"Beleza?"};
    
    auto t1 {make_tuple('a', ref(i), cref(s))};
    
    cout << "Valor de i antes: " << get<1>(t1) << endl;
    
    get<1>(t1) *= 2;
    
    cout << "Valor de i depois: " << i << endl;
    
    // ERRO! s foi armazenado
    // na tupla como uma referência constante, e por isso não pode alterado
    // seu valor alterado
    
    // get<2>(t1) = "Legal!"; 
}

Valor de i antes: 10
Valor de i depois: 20

No exemplo 4, vemos que ao modificar o valor da referência para i que estava armazenada na segunda posição da tupla t1, o valor do próprio i foi alterado, confirmando que guardamos de fato uma referência para a variável.

Além disso, a linha 24 – que está comentada para que o programa possa compilar – estaria em erro porque nela eu tento modificar o valor de uma referência constante (armazenada na terceira posição da tupla t1), o que não é permitido.


Como recuperar diversos valores em uma tupla de uma só vez?

Já vimos que para se obter o valor de um elemento em uma tupla, basta utilizar a função get com o índice do elemento e a tupla na entrada da função. Mas já pensou em fazer isso várias vezes, uma para cada elemento da tupla, para uma tupla com 10 elementos? O processo pode ser bastante repetitivo e aberto a erros de copiar-colar.

Felizmente, para a comodidade da nação programadora, há duas formas de se obter vários valores de uma tupla de uma só vez; elas são as seguintes: structured bindings e a função std::tie().

Structured bindings

A primeira forma de se recuperar vários elementos de uma tupla de uma única vez é através dos structured bindings, que significam grosseiramente associações estruturadas, em português. Com structured bindings, múltiplas variáveis podem ser criadas de uma vez, e elas armazenam os valores presentes na tupla. Também é possível criar referências com structured bindings (veja o quadro abaixo).

Sintaxe – structured bindings

auto [var1, var2, var3] = { tupla }
auto& [ref1, ref2, ref3] = { tupla }

Note que é preciso usar a quantidade exata de elementos presentes na tupla para o número de variáreis do structured bindings, caso contrário haverá um erro. Uma tupla de 3 elementos precisa ter 3 variáveis no structured bindings (mesmo que você não queira usar alguma delas).

std::tie

A segunda forma é o uso do std::tie, que permite fazer a mesma coisa que o structured bindings, mas possui mais flexibilidade ao permitir que se ignore elementos indesejados da tupla usando std::ignore no lugar do nome da variável indesejada.

Vejamos como utilizar o std::tie no exemplo a seguir.

Exemplo 5 – Structured bindings com tuplas em C++

#include <tuple>
#include <iostream>
#include <functional>

using namespace std;

int main()
{
    tuple t1 {10, "Ok"s, false};
    auto [i, str, b] = t1;
    
    cout << "Antes - i: " << i << ", str: \"" << str << "\", b: " << boolalpha << b << endl;

    tuple t2 {5, "Structure binding"s, true};
    tie(i, str, ignore) = t2;
    
    cout << "Depois - i: " << i << ", str: \"" << str << "\", b: " << boolalpha << b << endl;
}

Antes – i: 10, str: “Ok”, b: false
Depois – i: 5, str: “Structure binding”, b: false


Próximos assuntos

Agora que conhecemos as tuplas em C++ e sabemos como utilizá-las, para os próximos passos é útil saber como utilizar todos os valores da tupla de uma única vez para inicializar objetos e invocar funções, passando os elementos da tupla como argumentos. Falaremos, então, de make_from_tuple e apply. Também mostrarei como concatenar tuplas e comparar seu elementos.

Foto de perfil de Emanoel

Sou apaixonado por tecnologia, literatura e também filosofia. O cultivo dessas paixões ao longo da minha trajetória me inspiraram a compartilhar aquilo que aprendo com os outros da maneira mais clara que eu possa. Sou formado em Engenharia Elétrica no Brasil, e também sou engenheiro formado na França. Trabalho atualmente como programador C++ em uma multinacional francesa, uma das maiores empresas de TI do mundo.

Leave a Reply

Your email address will not be published. Required fields are marked *