C++ optional e variant C++17

A lojinha da Ana tem uma função que devolve o preço de um produto. E se o produto não existir? Devolver 0 mente ("é de graça?"); devolver -1 é um código secreto que alguém vai esquecer de conferir. O C++17 trouxe uma resposta honesta: std::optional, uma caixa que pode estar vazia. Na mesma leva veio o std::variant, uma caixa que guarda um tipo ou outro (um número ou um texto, por exemplo). E o C++23 juntou as duas ideias no std::expected: "um resultado ou o motivo do erro".

Você vai aprender
  • Devolver "talvez um valor" com std::optional e std::nullopt.
  • Conferir e ler: has_value(), if (opt), value(), *opt e value_or(…).
  • O que acontece quando você lê uma caixa vazia: std::bad_optional_access (o erro real).
  • Guardar um tipo ou outro com std::variant: std::holds_alternative, std::get e o erro std::bad_variant_access.
  • Tratar todos os tipos de uma vez com std::visit (o básico).
  • Conhecer o std::expected C++23.

Os programas foram compilados com o g++ 15.2 na máquina fictícia da Ana, na pasta ~/cpp, com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa. Alguns exemplos usam try e catch, da aula Exceções.


A ideia: a caixa que pode estar vazia, e a caixa de duas formas

  • optional é como a caixinha de correio da Ana: ou tem uma carta, ou está vazia. Antes de ler a carta, ela olha se tem alguma.
  • variant é como um pote com tampa onde cabe ou açúcar ou sal, nunca os dois. Antes de usar, ela confere a etiqueta para saber o que tem dentro.
std::optional<T>std::variant<A, B, …>
Pode conterUm T ou nadaUm A ou um B ou …
Pergunta antes de usar"Tem alguma coisa?""Qual dos tipos está aí?"
Cabeçalho<optional><variant>
Uso típicoBusca que pode não achar, campo opcional de um cadastroCélula de planilha (número ou texto), resposta de vários formatos

1. std::optional: talvez um valor

#include <iostream>
#include <optional>
#include <string>

std::optional<double> preco_de(const std::string& produto) {
    if (produto == "pão") return 0.75;
    if (produto == "leite") return 5.49;
    return std::nullopt;
}

int main() {
    std::optional<double> p1 = preco_de("leite");
    std::optional<double> p2 = preco_de("caviar");

    std::cout << std::boolalpha;
    std::cout << "leite tem preço? " << p1.has_value() << '\n';
    std::cout << "caviar tem preço? " << p2.has_value() << '\n';

    if (p1.has_value()) {
        std::cout << "Leite: R$ " << p1.value() << '\n';
    }
    if (p1) {
        std::cout << "Leite (com *): R$ " << *p1 << '\n';
    }
    if (!p2) {
        std::cout << "Caviar: não vendemos\n";
    }

    std::cout << "Caviar ou 0: " << p2.value_or(0.0) << '\n';
}
$ g++ -std=c++23 -Wall -Wextra buscar.cpp -o buscar
$ ./buscar
leite tem preço? true
caviar tem preço? false
Leite: R$ 5.49
Leite (com *): R$ 5.49
Caviar: não vendemos
Caviar ou 0: 0
std::optional<double>
"Talvez um double." É o tipo de retorno da função.
return 0.75;
Com valor: o double entra na caixa sozinho.
return std::nullopt;
Sem valor: devolve a caixa vazia (null option, "opção nula").
p1.has_value()
"Tem valor?" Responde true ou false.
if (p1)
Atalho para a mesma pergunta: o optional vira true quando tem valor. if (!p2) = "se está vazio".
p1.value()
Pega o valor. Se a caixa estiver vazia, lança uma exceção (seção seguinte).
*p1
Também pega o valor, sem conferir nada. Use só depois de ter conferido.
p2.value_or(0.0)
"O valor, ou então 0.0": nunca falha, porque sempre há uma resposta.

A grande vantagem aparece em quem lê a função: o tipo std::optional<double> já avisa, na assinatura, "posso não ter resposta". Com um double comum e o código secreto -1, nada obriga quem chama a se lembrar disso.

Experimente: rode e veja os dois resultados. Depois acrescente um terceiro produto na função (por exemplo, "café" a 18.90) e busque por ele. Por fim, troque o value_or(0.0) por value_or(-1.0) e veja a diferença.

#include <iostream>
#include <optional>
#include <string>

std::optional<double> preco_de(const std::string& produto) {
    if (produto == "pão") return 0.75;
    if (produto == "leite") return 5.49;
    return std::nullopt;
}

int main() {
    for (std::string produto : {"pão", "chocolate"}) {
        auto preco = preco_de(produto);
        if (preco) {
            std::cout << produto << ": R$ " << *preco << '\n';
        } else {
            std::cout << produto << ": não vendemos\n";
        }
    }
    std::cout << "Sem conferir: " << preco_de("chocolate").value_or(0.0) << '\n';
}

Ler uma caixa vazia: std::bad_optional_access

O value() confere se há valor. Se não há, ele lança uma exceção do tipo std::bad_optional_access ("acesso ruim ao optional"). Se ninguém a capturar, o programa termina na hora:

#include <iostream>
#include <optional>

int main() {
    std::optional<int> idade;
    std::cout << "Antes\n";
    std::cout << idade.value() << '\n';
    std::cout << "Depois\n";
}
$ g++ -std=c++23 -Wall -Wextra value_vazio.cpp -o value_vazio
$ ./value_vazio
Antes
terminate called after throwing an instance of 'std::bad_optional_access'
  what():  bad optional access
Abortado                   (imagem do núcleo gravada) ./value_vazio
$ echo $?
134                                  « "Depois" nunca apareceu
std::optional<int> idade;
Um optional criado sem valor começa vazio.
terminate called after throwing an instance of 'std::bad_optional_access'
"terminate foi chamado depois que uma exceção do tipo bad_optional_access foi lançada": ninguém a capturou, e o programa foi encerrado.
what(): bad optional access
A mensagem que a exceção carrega.

No editor do guia, que usa outra biblioteca padrão (a libc++ do Clang), o mesmo programa mostra Antes e termina com libc++abi: terminating due to uncaught exception of type std::bad_optional_access: bad_optional_access. As palavras mudam; o significado é o mesmo.

Como toda exceção, ela pode ser capturada com try/catch (aula Exceções):

#include <iostream>
#include <optional>

int main() {
    std::optional<int> idade;
    try {
        std::cout << idade.value() << '\n';
    } catch (const std::bad_optional_access& e) {
        std::cout << "Erro: " << e.what() << '\n';
    }
}
$ g++ -std=c++23 -Wall -Wextra value_catch.cpp -o value_catch
$ ./value_catch
Erro: bad optional access

Mas o normal é nem chegar aí: confira com if (opt) antes, ou use value_or. E o *idade numa caixa vazia? Ele não confere nada: é comportamento indefinido. Sem otimização, o g++ 15 ainda pega o erro com uma asserção:

$ ./estrela_vazio                    « std::cout << *idade;  com idade vazio
/usr/include/c++/15/optional:1186: constexpr _Tp& std::optional<_Tp>::operator*() & [with _Tp = int]: Assertion 'this->_M_is_engaged()' failed.
Abortado                   (imagem do núcleo gravada) ./estrela_vazio

Engaged = "ocupado": a biblioteca conferiu "a caixa está ocupada?" e a resposta foi não. Com -O2 essa conferência some, e o programa mostraria lixo sem reclamar.

Forma de lerSe estiver vazioQuando usar
opt.value()Lança std::bad_optional_accessQuando vazio seria um erro de verdade, a tratar com catch
*optComportamento indefinidoLogo depois de um if (opt)
opt.value_or(padrão)Devolve o padrãoQuando existe um valor razoável para "não tem"

2. std::variant: um tipo ou outro

Um std::variant<int, std::string> guarda em cada momento um int ou uma std::string. Ele sempre sabe qual dos dois tem dentro:

#include <iostream>
#include <string>
#include <variant>

int main() {
    std::variant<int, std::string> resposta = 42;

    std::cout << std::boolalpha;
    std::cout << "É int? " << std::holds_alternative<int>(resposta) << '\n';
    std::cout << "Valor: " << std::get<int>(resposta) << '\n';
    std::cout << "Índice: " << resposta.index() << '\n';

    resposta = std::string("não sei");
    std::cout << "É string? " << std::holds_alternative<std::string>(resposta) << '\n';
    std::cout << "Valor: " << std::get<std::string>(resposta) << '\n';
    std::cout << "Índice: " << resposta.index() << '\n';
}
$ g++ -std=c++23 -Wall -Wextra variante.cpp -o variante
$ ./variante
É int? true
Valor: 42
Índice: 0                            « int é o 1º tipo da lista (posição 0)
É string? true
Valor: não sei
Índice: 1                            « string é o 2º (posição 1)
std::variant<int, std::string>
A lista de tipos possíveis, chamados de alternativas.
= 42;
Agora guarda um int. Atribuir outro valor troca o tipo: depois de resposta = std::string("não sei"); ele guarda uma string, e o 42 foi jogado fora.
std::holds_alternative<int>(resposta)
"Está guardando um int?" (holds = "segura", "contém").
std::get<int>(resposta)
Pega o valor como int. Se o que está guardado for de outro tipo, lança uma exceção (veja abaixo).
resposta.index()
A posição, na lista, do tipo guardado agora.

Repare que se escreveu std::string("não sei") e não só "não sei". Escrever o tipo deixa claro para o leitor (e para o compilador) qual alternativa você quer.

Pedir o tipo errado: std::bad_variant_access

#include <iostream>
#include <string>
#include <variant>

int main() {
    std::variant<int, std::string> resposta = std::string("não sei");
    std::cout << std::get<int>(resposta) << '\n';
}
$ g++ -std=c++23 -Wall -Wextra get_errado.cpp -o get_errado
$ ./get_errado
terminate called after throwing an instance of 'std::bad_variant_access'
  what():  std::get: wrong index for variant
Abortado                   (imagem do núcleo gravada) ./get_errado

Wrong index for variant = "índice errado para o variant": você pediu o int (posição 0), mas o que está lá é a string (posição 1). Com try/catch (const std::bad_variant_access& e), o programa mostraria Erro: std::get: wrong index for variant e continuaria (no editor do guia, o e.what() da libc++ diz só bad_variant_access). O jeito tranquilo é perguntar antes:

#include <iostream>
#include <string>
#include <variant>

void mostrar(const std::variant<int, std::string>& r) {
    if (std::holds_alternative<int>(r)) {
        std::cout << "Número: " << std::get<int>(r) << '\n';
    } else {
        std::cout << "Texto: " << std::get<std::string>(r) << '\n';
    }
}

int main() {
    mostrar(42);
    mostrar(std::string("não sei"));
}
$ g++ -std=c++23 -Wall -Wextra resposta.cpp -o resposta
$ ./resposta
Número: 42
Texto: não sei

std::visit: uma função para todos os tipos (o básico)

Quando há muitas alternativas, uma cadeia de if (holds_alternative…) fica longa. O std::visit ("visitar") chama uma função com o valor que estiver guardado, seja do tipo que for. A função mais simples para isso é uma lambda com parâmetro auto, que aceita qualquer tipo:

#include <iostream>
#include <string>
#include <variant>
#include <vector>

int main() {
    std::vector<std::variant<int, double, std::string>> celulas = {
        10, 3.5, std::string("Ana"), 7
    };

    for (const auto& c : celulas) {
        std::visit([](const auto& valor) {
            std::cout << "[" << valor << "] ";
        }, c);
    }
    std::cout << '\n';
}
$ g++ -std=c++23 -Wall -Wextra visitar.cpp -o visitar
$ ./visitar
[10] [3.5] [Ana] [7]

Imagine as células de uma planilha: cada uma tem um número inteiro, um número com vírgula ou um texto. O std::visit recebe a lambda e a célula c, olha qual tipo está guardado e chama a lambda com ele. Aqui funcionou porque o cout << sabe mostrar os três tipos. Tratar cada tipo de um jeito diferente dentro do visit exige técnicas de templates (if constexpr, citado em constexpr); por enquanto, para tratamentos diferentes, use o holds_alternative.


3. std::expected: um resultado ou o motivo do erro C++23

O optional diz "não achei", mas não diz por quê. O std::expected<T, E> ("esperado") guarda ou o resultado esperado (T) ou um erro (E), que pode ser uma mensagem. É uma alternativa às exceções para erros que acontecem com frequência, como um texto digitado errado:

#include <expected>
#include <iostream>
#include <string>

std::expected<int, std::string> idade_de(const std::string& texto) {
    if (texto.empty()) {
        return std::unexpected("texto vazio");
    }
    int idade = 0;
    for (char c : texto) {
        if (c < '0' || c > '9') {
            return std::unexpected("não é número: " + texto);
        }
        idade = idade * 10 + (c - '0');
    }
    return idade;
}

int main() {
    for (std::string t : {"29", "abc", ""}) {
        auto r = idade_de(t);
        if (r.has_value()) {
            std::cout << "Idade: " << r.value() << '\n';
        } else {
            std::cout << "Erro: " << r.error() << '\n';
        }
    }
}
$ g++ -std=c++23 -Wall -Wextra esperado.cpp -o esperado
$ ./esperado
Idade: 29
Erro: não é número: abc
Erro: texto vazio
std::expected<int, std::string>
"Espero um int; se der errado, você recebe uma std::string explicando."
return std::unexpected("…");
Devolve o lado do erro (unexpected = "inesperado").
return idade;
Devolve o lado do sucesso.
r.has_value() / r.value() / r.error()
Como no optional, mais o error() para ler o motivo.

Funciona no g++ 15 com -std=c++23. Com -std=c++20, o próprio g++ explica: error: ‘expected’ in namespace ‘std’ does not name a template type e logo abaixo note: ‘std::expected’ is only available from C++23 onwards ("só está disponível a partir do C++23"). Por ser recente, você vai ver pouco expected em código antigo; em projetos novos, ele é cada vez mais comum. (Para converter texto em número de verdade, use as funções da aula Texto ↔ Número; o laço acima é só para o exemplo.)


Qual usar?

SituaçãoUse
A resposta pode simplesmente não existir, e o motivo é óbvio (não achei)std::optional<T>
Pode dar errado, e quem chama precisa saber o motivostd::expected<T, E> (C++23) ou uma exceção
Um erro raro e grave, que deve interromper tudoExceção
O valor pode ser de tipos diferentes, todos válidosstd::variant<A, B, …>
Vários valores ao mesmo tempostruct, std::pair, std::tuple (aula pair e tuple)
E os ponteiros nulos?

Em código antigo, "talvez não exista" era feito com um ponteiro que podia ser nullptr (aula nullptr). Para devolver um valor, o optional é mais seguro: não há memória para gerenciar e o tipo diz claramente que pode estar vazio.


Quando dá errado

terminate called after throwing an instance of 'std::bad_optional_access'

O que significa: o programa chamou .value() num optional vazio e ninguém capturou a exceção.

Como consertar: confira com if (opt)/has_value() antes, use value_or(padrão), ou capture com catch (const std::bad_optional_access&).

terminate called after throwing an instance of 'std::bad_variant_access' / what(): std::get: wrong index for variant

O que significa: std::get<T> pediu um tipo diferente do que está guardado.

Como consertar: pergunte antes com std::holds_alternative<T>, ou use std::visit.

error: conversion from ‘double’ to non-scalar type ‘std::variant<int, …>’ requested
$ g++ -std=c++23 -Wall -Wextra variant_tipo.cpp -o variant_tipo
variant_tipo.cpp: In function ‘int main()’:
variant_tipo.cpp:5:40: error: conversion from ‘double’ to non-scalar type ‘std::variant<int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >’ requested
    5 |     std::variant<int, std::string> v = 3.5;
      |                                        ^~~
In file included from variant_tipo.cpp:2:
/usr/include/c++/15/variant: In instantiation of ‘constexpr _Tp& std::get(variant<_Types ...>&) [with _Tp = double; _Types = {int, __cxx11::basic_string<char, char_traits<char>, allocator<char> >}]’:
variant_tipo.cpp:6:28:   required from here
    6 |     return std::get<double>(v);
      |            ~~~~~~~~~~~~~~~~^~~
/usr/include/c++/15/variant:1150:42: error: static assertion failed: T must occur exactly once in alternatives
                                     « … e mais 280 linhas (a mensagem inteira tem 292)

O que significa: dois erros. Na linha 5, o variant não aceita 3.5: double não está na lista, e ele se recusa a cortar o número para caber no int. Na linha 6, std::get<double> pede um tipo que não é alternativa: T must occur exactly once in alternatives = "T precisa aparecer exatamente uma vez nas alternativas".

Como consertar: só use os tipos da lista. Se precisa de double, acrescente-o: std::variant<int, double, std::string>.


Referência rápida

EscritaO que faz
std::optional<T> o;Optional vazio (<optional>)
return valor; / return std::nullopt;Devolve cheio / vazio
o.has_value(), if (o)Tem valor?
o.value()O valor, ou lança std::bad_optional_access
*oO valor, sem conferir
o.value_or(x)O valor, ou x
o.reset(); / o = std::nullopt;Esvazia
std::variant<A, B> v = a;Guarda um A (<variant>)
std::holds_alternative<A>(v)Está guardando um A?
std::get<A>(v)O valor como A, ou lança std::bad_variant_access
v.index()Posição do tipo guardado
std::visit(f, v)Chama f com o valor guardado
std::expected<T, E>Resultado ou erro (C++23, <expected>)
return std::unexpected(erro);Devolve o lado do erro
r.error()Lê o erro

Exercícios

1. A caixa vazia

Complete para devolver um optional vazio:

return std::;

2. Com valor padrão

Complete para mostrar o desconto, ou 0 se não houver:

std::cout << desconto.(0);

3. O que acontece em std::optional<int> x; std::cout << x.value(); se ninguém usar try/catch?

4. Quanto vale v.index() em std::variant<int, double, std::string> v = 2.5;?

5. Uma função converte texto em data e quem chama precisa saber se o erro foi "mês inválido" ou "formato errado". Qual tipo de retorno combina melhor?

Missão final: procurar na chamada

Missão: complete posicao_de: percorra a lista e, se achar o nome, devolva a posição i; se terminar o laço sem achar, devolva std::nullopt. A saída deve ser:

Bruno está na posição 1
Zeca: não encontrado
#include <iostream>
#include <optional>
#include <string>
#include <vector>

std::optional<int> posicao_de(const std::vector<std::string>& lista, const std::string& nome) {
    // percorra lista; achou? return i;
    // não achou? return std::nullopt;
    return 0;
}

int main() {
    std::vector<std::string> turma = {"Ana", "Bruno", "Carla"};

    for (std::string nome : {"Bruno", "Zeca"}) {
        auto pos = posicao_de(turma, nome);
        if (pos) {
            std::cout << nome << " está na posição " << *pos << '\n';
        } else {
            std::cout << nome << ": não encontrado\n";
        }
    }
}
#include <iostream>
#include <optional>
#include <string>
#include <vector>

std::optional<int> posicao_de(const std::vector<std::string>& lista, const std::string& nome) {
    for (int i = 0; i < static_cast<int>(lista.size()); ++i) {
        if (lista[i] == nome) {
            return i;
        }
    }
    return std::nullopt;
}

int main() {
    std::vector<std::string> turma = {"Ana", "Bruno", "Carla"};

    for (std::string nome : {"Bruno", "Zeca"}) {
        auto pos = posicao_de(turma, nome);
        if (pos) {
            std::cout << nome << " está na posição " << *pos << '\n';
        } else {
            std::cout << nome << ": não encontrado\n";
        }
    }
}

A solução, no laboratório:

$ g++ -std=c++23 -Wall -Wextra posicao.cpp -o posicao
$ ./posicao
Bruno está na posição 1
Zeca: não encontrado

Resumo

  • std::optional<T>: um T ou nada. Devolva std::nullopt para "nada".
  • Confira com if (opt) ou has_value(); leia com *opt (depois de conferir), value() (lança std::bad_optional_access se vazio) ou value_or(padrão).
  • std::variant<A, B>: um A ou um B. Pergunte com std::holds_alternative<A>; leia com std::get<A> (lança std::bad_variant_access se for o outro).
  • std::visit(lambda, v) trata o valor guardado, qualquer que seja o tipo.
  • std::expected<T, E> (C++23): o resultado ou o motivo do erro, com std::unexpected e error().
  • Os três deixam claro, no tipo, o que pode acontecer, em vez de códigos secretos como -1.