Capítulo 14: Tratamento de Erros Moderno: noexcept, std::optional e std::variant

Especialização em Sistemas com C++ • C++20 & C++23 • RAII, Smart Pointers, Templates, Concorrência e Alta Performance


🗺️ Mapa Conceitual de Estratégias de Tratamento de Erros em Sistemas

flowchart TD
    Erro["Gestão de Erros e Casos Excepcionais"] --> Excecoes["Exceções Tradicionais (try / catch)"]
    Erro --> SemExcecoes["Fluxo Moderno Funcional / Algébrico"]

    subgraph TRAD_EXC["Exceções Tradicionais"]
        SlowPath["Caminho Lento (Tabela DWARF / SEH Unwinding)"]
        NoexceptOpt["noexcept: Permite Otimizações Críticas em Move"]
    end

    subgraph MODERN_ALT["Alternativas Zero-Overhead Modernas"]
        StdOptional["std::optional<T> (Sucesso com Valor OU Ausência Segura)"]
        StdVariant["std::variant<T1, T2> (Tipo Soma Algébrico / Result Pattern)"]
        StdVisit["std::visit com Overload Pattern (Pattern Matching Elegante)"]
    end

    Excecoes --> TRAD_EXC
    SemExcecoes --> MODERN_ALT

    style Erro fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style TRAD_EXC fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style MODERN_ALT fill:#e8f5e9,stroke:#4caf50,stroke-width:2px

🏛️ 1. O Custo Oculto das Exceções e o Papel do noexcept

Em C++, o modelo de tratamento de exceções é construído sobre o princípio de “Zero Overhead para o Caminho Feliz”: enquanto nenhuma exceção for lançada, nenhuma penalidade de tempo de CPU é incorrida. No entanto, quando uma exceção é efetivamente lançada (throw), o sistema operacional e a biblioteca de runtime precisam percorrer estruturas complexas de metadados de desenrolamento de pilha (Stack Unwinding, via tabelas DWARF no Linux ou SEH no Windows).

Esse processo pode levar de centenas a milhares de microssegundos, o que é inaceitável em laços críticos de motores gráficos, telecomunicações ou sistemas de negociação de alta frequência (HFT).

A Importância Crítica do noexcept:

Marcar uma função com noexcept informa ao compilador que ela garante categoricamente nunca deixar escapar uma exceção. Isso permite que:

  1. O compilador elimine totalmente as tabelas de salvaguarda de pilha naquela função.
  2. Contêineres como std::vector utilizem o Construtor de Movimento em vez de cópia durante operações de redimensionamento (reserve / push_back). Sem noexcept, std::vector faz cópias lentas para preservar a garantia forte de exceção!

⚙️ 2. std::optional: Adeus a Ponteiros Nulos e Códigos Mágicos

Historicamente, funções que poderiam não retornar um valor utilizavam valores sentinela perigosos (como retornar -1 ou ponteiros nulos nullptr). O std::optional<T> modela explicitamente a presença ou ausência de um valor sem nenhuma alocação de memória no Heap:

#include <optional>
#include <string>

std::optional<std::string> buscar_configuracao(std::string_view chave) {
    if (chave == "timeout") return "5000";
    return std::nullopt; // Ausência expressa e inequívoca
}

void uso() {
    auto res = buscar_configuracao("porta");
    
    // Método funcional seguro com fallback:
    std::string porta = res.value_or("8080");
}

📦 3. std::variant e o Padrão de Sobrecarga (Overload Pattern)

O std::variant<Ts...> é uma união tipada e segura (type-safe tagged union) que pode conter exatamente um dentre múltiplos tipos em qualquer instante. É a fundação para implementar o padrão Result (comum em linguagens como Rust e Swift) em C++:

#include <variant>
#include <iostream>

struct Sucesso { double resultado; };
struct ErroDivisaoPorZero {};

using DivisaoResult = std::variant<Sucesso, ErroDivisaoPorZero>;

DivisaoResult dividir(double a, double b) noexcept {
    if (b == 0.0) return ErroDivisaoPorZero{};
    return Sucesso{ a / b };
}

// O Idioma Overload Pattern para Dispatch Elegante
template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
template<class... Ts> overload(Ts...) -> overload<Ts...>;

void processar_calculo(double a, double b) {
    std::visit(overload {
        [](const Sucesso& s) { std::cout << "Resultado: " << s.resultado << '\n'; },
        [](const ErroDivisaoPorZero&) { std::cerr << "Falha: Divisao por zero!\n"; }
    }, dividir(a, b));
}

📊 4. Matriz Comparativa: Estratégias de Erro em C++

Estratégia Custo do Caminho de Sucesso Custo em Caso de Erro Alocação no Heap? Recomendação de Uso
throw / catch Praticamente Zero Altíssimo (milhares de ciclos) Sim (objeto exceção) Falhas genuinamente catastróficas e raras
std::optional Quase Zero Mínimo ($O(1)$) Não (in-place na Stack) Busca e consultas onde a ausência é esperada
std::variant Quase Zero Mínimo ($O(1)$) Não (tamanho da maior variante) Retornos com detalhes ricos de erro funcional
noexcept Máxima Otimização N/A (se falhar, chama std::terminate) Não Construtores de movimento e funções atômicas