No artigo Quando o Contrato se Quebra — Exceções e a Aliança com o RAII vimos as exceções e a diretriz que as governa: elas servem para erros excepcionais, não para condições esperadas e corriqueiras. Mas isso deixou uma pergunta prática no ar. Como uma função comunica, de forma limpa, que às vezes simplesmente não tem um valor a devolver — uma busca que não encontrou nada, uma conversão que não deu certo, um campo opcional que estava vazio? Em C, os truques eram todos frágeis: devolver um ponteiro que pode ser nulo, reservar um valor "mágico" como -1 para significar "nada", ou usar um parâmetro de saída com um bool de sucesso. Cada um tem armadilhas. O C++17 trouxe uma resposta elegante e segura: o std::optional, um tipo que representa explicitamente "um valor que pode ou não estar presente". Hoje você aprende a usá-lo, e ele vai mudar como você projeta funções que às vezes não têm resposta.
O problema dos valores mágicos e ponteiros nulos
Pense numa função que procura a posição de um caractere numa string e devolve o índice. E se o caractere não existir? A tradição do C oferece saídas ruins:
// Abordagem C, problemática: -1 significa "não achou".
int acha(const std::string& s, char c) {
for (int i = 0; i < (int)s.size(); ++i)
if (s[i] == c) return i;
return -1; // valor "mágico" — e se -1 fosse um índice válido em outro contexto?
}
// O chamador PRECISA saber que -1 é especial, e lembrar de checá-lo.
// Se esquecer, usa -1 como índice e causa comportamento indefinido.
O problema é que -1 não carrega a informação "isto é ausência" — é só um número que, por convenção, significa erro. Nada obriga o chamador a checá-lo, e nada impede que ele use o -1 como se fosse um índice real. A std::string::npos que você viu na Fase 1 é uma versão dessa mesma ideia — um valor sentinela. Funciona, mas depende da disciplina de quem chama. O optional torna a ausência parte do tipo.
optional: a ausência vira parte do tipo
Um std::optional<T> é uma caixa que ou contém um T, ou está vazia. O tipo em si anuncia "pode não haver valor aqui", e o compilador ajuda a garantir que você lide com os dois casos:
#include <iostream>
#include <optional> // std::optional, std::nullopt (C++17)
#include <string>
// O tipo de retorno JÁ DIZ: "pode ou não haver um índice".
std::optional<int> acha(const std::string& s, char c) {
for (int i = 0; i < (int)s.size(); ++i)
if (s[i] == c)
return i; // devolve um valor presente
return std::nullopt; // devolve "vazio" explicitamente
}
int main() {
std::string texto = "programacao";
auto pos = acha(texto, 'g');
if (pos) { // um optional converte para bool: tem valor?
std::cout << "achei em " << *pos << '\n'; // *pos acessa o valor: achei em 3
} else {
std::cout << "não achei\n";
}
auto pos2 = acha(texto, 'z');
if (!pos2)
std::cout << "'z' não está no texto\n";
return 0;
}
O retorno std::optional<int> comunica na assinatura que a função pode não ter resposta. Dentro, return i; devolve um valor presente e return std::nullopt; devolve o vazio explícito. No uso, um optional converte para bool (verdadeiro se tem valor), e *pos acessa o valor guardado — a mesma sintaxe de desreferência dos ponteiros, mas sem os perigos deles. A diferença crucial em relação ao -1: o tipo obriga o leitor a reconhecer que pode não haver valor, e acessar o valor exige um passo deliberado.
Acessando com segurança: value(), value_or() e o perigo do *
Há três formas de pegar o valor de dentro, e a distinção importa. *opt e opt-> acessam diretamente, mas são comportamento indefinido se o optional estiver vazio — use-os só depois de checar. opt.value() acessa com verificação: lança std::bad_optional_access se estiver vazio (a rede de segurança das exceções da aula passada). E opt.value_or(padrao) devolve o valor se presente, ou um padrão que você fornece se vazio — muitas vezes a forma mais limpa:
#include <iostream>
#include <optional>
#include <string>
std::optional<std::string> nome_do_usuario(int id) {
if (id == 1) return "Ana";
return std::nullopt; // usuário desconhecido
}
int main() {
// value_or: dá um padrão elegante quando não há valor.
std::cout << nome_do_usuario(1).value_or("desconhecido") << '\n'; // Ana
std::cout << nome_do_usuario(99).value_or("desconhecido") << '\n'; // desconhecido
// value(): lança se vazio (útil quando ausência seria realmente um erro).
auto n = nome_do_usuario(1);
std::cout << n.value() << '\n'; // Ana
// * : rápido, mas SÓ após checar. Desreferenciar vazio = comportamento indefinido.
if (n) std::cout << *n << '\n'; // seguro aqui, porque checamos
return 0;
}
value_or é especialmente útil: nome_do_usuario(99).value_or("desconhecido") expressa "o nome, ou 'desconhecido' se não houver" em uma linha legível. A regra de bolso: use value_or quando há um padrão sensato, value() quando a ausência deveria ser um erro tratável, e */-> apenas depois de confirmar a presença com um if.
Quando usar optional (e quando não)
Prometo sempre a medida. O optional brilha para representar ausência esperada e legítima de um valor único: uma busca que pode não achar, um campo de configuração que pode não estar preenchido, uma conversão que pode falhar sem que isso seja catastrófico. Ele deixa a assinatura honesta e força o tratamento dos dois casos. Mas ele não é para tudo. Se a "ausência" na verdade carrega o motivo da falha (não só "não deu", mas "não deu porque o arquivo não existe" versus "porque não há permissão"), o optional é insuficiente — ele diz que não há valor, mas não por quê. Para isso existe o std::expected (C++23), que veremos no próximo artigo. E se a condição for um erro verdadeiramente excepcional, uma exceção (artigo Quando o Contrato se Quebra — Exceções e a Aliança com o RAII) ainda é a escolha. O optional ocupa o meio-termo valioso: "pode não haver valor, e isso é normal, e não preciso explicar por quê".
O optional move a ausência para dentro do tipo: em vez de combinar que -1 significa "não achei", a assinatura passa a dizer que pode não haver resultado, e o compilador cobra que isso seja tratado. O acesso é onde mora o risco — value_or quando existe um padrão razoável, value() quando a ausência é excepcional o bastante para justificar exceção, e * apenas depois de confirmar que há valor, porque desreferenciar um optional vazio é comportamento indefinido.
Fontes e leituras recomendadas
- cppreference.com/w/cpp/utility/optional: a referência completa de
std::optional, comvalue,value_or,has_valuee as regras de acesso. - Bjarne Stroustrup, A Tour of C++ (3ª ed.), seção sobre
optional,varianteany: a apresentação dos tipos-soma do C++ moderno. - cppreference.com/w/cpp/utility/optional/value_or: os detalhes de
value_or, a forma mais elegante de fornecer um padrão. - ISO C++ Core Guidelines, regra F.60 e discussões sobre retorno de valores opcionais: quando preferir
optionala ponteiros ou sentinelas. - cppreference.com/w/cpp/error/bad_optional_access: a exceção lançada por
value()em optional vazio, ligando ao artigo Quando o Contrato se Quebra — Exceções e a Aliança com o RAII.
Exercícios
Exercício 1
Escreva uma função std::optional<double> divide_segura(double a, double b) que devolva o resultado da divisão, ou std::nullopt se b for zero. Demonstre os dois casos no main.
Ver resposta
✓ Resposta: Divisão segura:
#include <iostream>
#include <optional>
std::optional<double> divide_segura(double a, double b) {
if (b == 0.0) return std::nullopt;
return a / b;
}
int main() {
if (auto r = divide_segura(10, 2))
std::cout << "resultado: " << *r << '\n'; // resultado: 5
if (!divide_segura(1, 0))
std::cout << "divisão por zero evitada\n";
return 0;
}
O if (auto r = divide_segura(10, 2)) declara e testa o optional numa linha — idioma comum.
Exercício 2
Explique por que devolver std::optional<int> é mais seguro que devolver -1 para sinalizar "não encontrado". Cite pelo menos dois problemas do -1 que o optional resolve.
Ver resposta
✓ Resposta: Devolver std::optional<int> é mais seguro que -1 por, ao menos, dois motivos. Primeiro, o tipo carrega a informação de ausência: optional<int> anuncia na assinatura que pode não haver valor, obrigando o leitor e o compilador a reconhecerem os dois casos; -1 é só um int comum, e nada no tipo avisa que ele é especial. Segundo, elimina a colisão com valores válidos: -1 pode ser um resultado legítimo em muitos contextos (um saldo, uma diferença, um deslocamento), então usá-lo como "erro" é ambíguo; o estado vazio do optional é distinto de qualquer valor de int, sem sobreposição. Um terceiro: acessar o valor de um optional exige um passo deliberado (*, value()), enquanto usar -1 acidentalmente como índice é fácil e causa comportamento indefinido silencioso.
Exercício 3
Dada a função nome_do_usuario da aula, escreva três acessos ao resultado: um com value_or, um com value() dentro de um try/catch, e um com * protegido por if. Comente quando cada forma é apropriada.
Ver resposta
✓ Resposta: Os três acessos:
#include <iostream>
#include <optional>
#include <string>
// (nome_do_usuario da aula)
int main() {
// value_or: apropriado quando há um padrão sensato para "ausente".
std::cout << nome_do_usuario(99).value_or("visitante") << '\n';
// value() em try/catch: apropriado quando a ausência é um erro a tratar.
try {
std::cout << nome_do_usuario(99).value() << '\n';
} catch (const std::bad_optional_access&) {
std::cout << "usuário inexistente\n";
}
// * com if: apropriado quando você já vai ramificar no fluxo mesmo.
auto n = nome_do_usuario(1);
if (n) std::cout << *n << '\n';
return 0;
}
value_or é ideal quando um valor padrão faz sentido; value() com try/catch quando a ausência é excepcional o bastante para justificar tratamento explícito; * com if quando o fluxo já se divide naturalmente entre "tem" e "não tem".
Exercício 4
O código abaixo tem um bug perigoso. Aponte-o e corrija.
#include <optional>
#include <iostream>
std::optional<int> busca(int chave); // pode devolver vazio
int main() {
auto r = busca(42);
std::cout << *r << '\n'; // e se busca não achou?
return 0;
}
Ver resposta
✓ Resposta: O bug: *r desreferencia o optional sem checar se ele tem valor. Se busca(42) devolveu std::nullopt, *r é comportamento indefinido — acesso a um valor inexistente. Correções possíveis:
#include <optional>
#include <iostream>
std::optional<int> busca(int chave);
int main() {
auto r = busca(42);
if (r) // checa antes de desreferenciar
std::cout << *r << '\n';
else
std::cout << "não encontrado\n";
// ou, em uma linha, com padrão:
std::cout << busca(42).value_or(-1) << '\n';
return 0;
}
A primeira forma trata os dois casos; a segunda fornece um padrão. Ambas eliminam o acesso desprotegido.
Exercício 5
Discuta a fronteira entre std::optional e exceções. Dê um exemplo de situação em que optional é a escolha certa e outra em que uma exceção seria mais apropriada, justificando com a diretriz do artigo.
Ver resposta
✓ Resposta: A fronteira: std::optional serve para ausência esperada e normal de um valor, onde "não há resultado" é um desfecho legítimo e comum; exceções servem para erros excepcionais que impedem a função de cumprir seu contrato. Exemplo em que optional é a escolha certa: procurar um usuário por ID num cache — não achar é rotineiro (o usuário pode simplesmente não estar no cache), faz parte do funcionamento normal, e o chamador naturalmente lida com "achou/não achou"; um optional<Usuario> expressa isso com clareza e sem o custo de uma exceção. Exemplo em que exceção é mais apropriada: abrir o banco de dados na inicialização do programa — falhar aqui é raro e catastrófico, impede o sistema de operar, e não há "caminho normal" que siga sem o banco; lançar uma exceção interrompe limpamente e sinaliza a gravidade. A diretriz: se a ausência é um resultado comum e sem drama, optional; se é uma falha rara que quebra o contrato, exceção.