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".
- Devolver "talvez um valor" com
std::optionalestd::nullopt. - Conferir e ler:
has_value(),if (opt),value(),*optevalue_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::gete o errostd::bad_variant_access. - Tratar todos os tipos de uma vez com
std::visit(o básico). - Conhecer o
std::expectedC++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 conter | Um T ou nada | Um A ou um B ou … |
| Pergunta antes de usar | "Tem alguma coisa?" | "Qual dos tipos está aí?" |
| Cabeçalho | <optional> | <variant> |
| Uso típico | Busca que pode não achar, campo opcional de um cadastro | Cé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
doubleentra na caixa sozinho. - return std::nullopt;
- Sem valor: devolve a caixa vazia (null option, "opção nula").
- p1.has_value()
- "Tem valor?" Responde
trueoufalse. - if (p1)
- Atalho para a mesma pergunta: o optional vira
truequando 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'
- "
terminatefoi chamado depois que uma exceção do tipobad_optional_accessfoi 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 ler | Se estiver vazio | Quando usar |
|---|---|---|
opt.value() | Lança std::bad_optional_access | Quando vazio seria um erro de verdade, a tratar com catch |
*opt | Comportamento indefinido | Logo depois de um if (opt) |
opt.value_or(padrão) | Devolve o padrão | Quando 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 deresposta = 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 umastd::stringexplicando." - 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 oerror()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ção | Use |
|---|---|
| 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 motivo | std::expected<T, E> (C++23) ou uma exceção |
| Um erro raro e grave, que deve interromper tudo | Exceção |
| O valor pode ser de tipos diferentes, todos válidos | std::variant<A, B, …> |
| Vários valores ao mesmo tempo | struct, std::pair, std::tuple (aula pair e tuple) |
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
| Escrita | O 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 |
*o | O 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:
2. Com valor padrão
Complete para mostrar o desconto, ou 0 se não houver:
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>: umTou nada. Devolvastd::nulloptpara "nada".- Confira com
if (opt)ouhas_value(); leia com*opt(depois de conferir),value()(lançastd::bad_optional_accessse vazio) ouvalue_or(padrão). std::variant<A, B>: umAou umB. Pergunte comstd::holds_alternative<A>; leia comstd::get<A>(lançastd::bad_variant_accessse 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, comstd::unexpectedeerror().- Os três deixam claro, no tipo, o que pode acontecer, em vez de códigos secretos como
-1.