C++ Exceções (try e catch)

Nem tudo dá certo: o arquivo não existe, o usuário digitou "abc" onde devia ir um número, a conta não tem saldo para o saque. Uma função que encontra um problema desses muitas vezes não sabe o que fazer com ele (mostrar uma mensagem? tentar de novo? desistir?), mas quem a chamou sabe. As exceções são o jeito de o C++ levar o problema de quem o encontrou até quem sabe tratá-lo. Nesta aula você vai lançar erros com throw, pegá-los com try e catch e ver o que acontece quando ninguém pega.

Você vai aprender
  • Como lançar um erro: throw std::invalid_argument("…");.
  • Como pegar o erro e continuar: try { … } catch (const std::exception& e) { … }.
  • O que acontece quando ninguém pega: terminate called after throwing an instance of ….
  • A família std::exception e as filhas mais usadas: runtime_error, invalid_argument, out_of_range.
  • Como ler a mensagem com e.what(), e por que pegar por const&.
  • Como usar vários catch, a ordem certa entre eles e o catch (...).
  • Como criar a sua própria exceção: class SaldoInsuficiente : public std::runtime_error.

Todos os programas desta aula foram compilados com o g++ 15 na máquina fictícia da Ana, com o comando padrão do guia: g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa. Você pode repetir cada um no editor do guia.


A ideia: o alarme de incêndio

Num prédio, quem vê fumaça no 7º andar não precisa saber apagar incêndio. A pessoa aperta o alarme, e o aviso sobe até quem sabe agir: a brigada, os bombeiros. Enquanto isso, todo mundo para o que estava fazendo.

No prédioEm C++
Ver a fumaça e apertar o alarmethrow ("lançar"): a função encontra o problema e lança uma exceção.
O aviso que sobe (com o andar e o que está pegando fogo)O objeto exceção, com uma mensagem dentro.
A área vigiada pela brigadatry ("tentar"): um bloco de código cujos alarmes você quer atender.
A brigada que atendecatch ("pegar"): o código que trata o problema.
Ninguém atende o alarmeO programa é abortado.

1. throw, try e catch

#include <iostream>
#include <stdexcept>

double dividir(double a, double b) {
    if (b == 0) {
        throw std::invalid_argument("divisão por zero");
    }
    return a / b;
}

int main() {
    try {
        std::cout << dividir(10, 4) << '\n';
        std::cout << dividir(10, 0) << '\n';
        std::cout << "Esta linha não aparece.\n";
    } catch (const std::invalid_argument& e) {
        std::cout << "Erro: " << e.what() << '\n';
    }
    std::cout << "O programa continua.\n";
}
$ g++ -std=c++23 -Wall -Wextra dividir.cpp -o dividir
$ ./dividir
2.5
Erro: divisão por zero
O programa continua.
#include <stdexcept>
Traz as exceções padrão (standard exceptions), como std::invalid_argument.
throw std::invalid_argument("divisão por zero");
Cria uma exceção do tipo std::invalid_argument ("argumento inválido"), com a mensagem entre parênteses, e a lança. A função para ali mesmo: o return a / b; não roda.
try { … }
O bloco vigiado. Se qualquer coisa lá dentro lançar uma exceção, inclusive uma função chamada lá dentro, o resto do bloco é pulado. Por isso Esta linha não aparece. não apareceu.
catch (const std::invalid_argument& e)
Diz qual tipo de exceção este bloco atende e dá um nome a ela: e. O const& pega a exceção original, sem copiar (seção 4).
e.what()
Devolve a mensagem que foi colocada na exceção (what = "o quê": "o que aconteceu?").
O programa continua.
Depois do catch, o programa segue normalmente pela linha de baixo. O erro foi tratado.

Repare na ordem dos acontecimentos: dividir(10, 4) deu certo e mostrou 2.5; dividir(10, 0) lançou; o resto do try foi pulado; o catch rodou; o programa seguiu.

A exceção atravessa funções

O grande poder das exceções: a função que lança e o catch que trata podem estar longe um do outro. A exceção "sobe" pelas funções, pulando o resto de cada uma, até achar um try:

#include <iostream>
#include <stdexcept>

void c() {
    std::cout << "c: começou\n";
    throw std::runtime_error("problema em c");
    std::cout << "c: terminou\n";
}

void b() {
    std::cout << "b: começou\n";
    c();
    std::cout << "b: terminou\n";
}

int main() {
    try {
        b();
    } catch (const std::exception& e) {
        std::cout << "main pegou: " << e.what() << '\n';
    }
}
$ g++ -std=c++23 -Wall -Wextra fluxo.cpp -o fluxo
$ ./fluxo
b: começou
c: começou
main pegou: problema em c          « "c: terminou" e "b: terminou" não aparecem

O main chamou b, que chamou c, que lançou. Nem c nem b tinham try, então as duas foram interrompidas no meio, e a exceção chegou ao main. As variáveis locais das funções interrompidas são destruídas direitinho no caminho: um arquivo aberto em b, por exemplo, seria fechado.


2. Quando ninguém pega a exceção

E se não houver try nenhum? O mesmo programa da divisão, sem o try:

#include <iostream>
#include <stdexcept>

double dividir(double a, double b) {
    if (b == 0) {
        throw std::invalid_argument("divisão por zero");
    }
    return a / b;
}

int main() {
    std::cout << dividir(10, 4) << '\n';
    std::cout << dividir(10, 0) << '\n';
    std::cout << "Fim.\n";
}
$ g++ -std=c++23 -Wall -Wextra semtry.cpp -o semtry
$ ./semtry
2.5
terminate called after throwing an instance of 'std::invalid_argument'
  what():  divisão por zero
Abortado                   (imagem do núcleo gravada) ./semtry
$ echo $?
134

Linha por linha:

LinhaQuem escreveuO que significa
terminate called after throwing an instance of 'std::invalid_argument'A biblioteca do C++"terminate foi chamado depois do lançamento de uma std::invalid_argument". A exceção subiu até sair do main, e o C++ chamou a função de emergência std::terminate, que encerra o programa.
what(): divisão por zeroA biblioteca do C++A mensagem de dentro da exceção, o mesmo texto que e.what() devolveria. É a pista mais útil.
Abortado (imagem do núcleo gravada) ./semtryO BashO programa foi abortado (morto pelo sinal SIGABRT). "Imagem do núcleo gravada" (core dumped, num sistema em inglês) quer dizer que o sistema pode ter salvo uma cópia da memória do programa para análise.
134O Bash (echo $?)O código de saída: 128 + 6, onde 6 é o número do sinal SIGABRT. Sempre que você vir 134, pense "abortado".

As duas primeiras linhas saem em inglês em qualquer sistema: são escritas pela biblioteca do C++. A terceira vem do terminal e segue o idioma do sistema.

No editor do guia, a mensagem é outra

O editor usa o Clang com outra biblioteca do C++ (a libc++, do projeto LLVM; o g++ usa a libstdc++, do projeto GNU). O mesmo programa termina lá com uma linha só: libc++abi: terminating due to uncaught exception of type std::invalid_argument: divisão por zero ("encerrando por causa de uma exceção não pega do tipo…"), e o código de saída também é 134. Palavras diferentes, mesma informação: o tipo da exceção e a mensagem.

Linhas de cout podem sumir quando o programa aborta

No terminal, o 2.5 apareceu antes do erro. Mas se a saída do programa for para um arquivo ou outro programa (./semtry > saida.txt), o cout guarda o texto num buffer e o aborto pode impedir que ele seja escrito. Mais um motivo para tratar as exceções.

Missão: rode este programa e veja ele abortar na turma B, que está vazia. Coloque as quatro linhas das médias dentro de um try e trate a std::invalid_argument com um catch que mostra Erro: seguido de e.what(). No fim, o programa deve mostrar Fim. e terminar com código 0.

#include <iostream>
#include <stdexcept>
#include <vector>

double media(const std::vector<double>& notas) {
    if (notas.empty()) {
        throw std::invalid_argument("lista vazia");
    }
    double soma = 0.0;
    for (double n : notas) {
        soma += n;
    }
    return soma / notas.size();
}

int main() {
    std::vector<double> turma_a = {8.5, 6.0, 9.5};
    std::vector<double> turma_b = {};

    double a = media(turma_a);
    std::cout << "Turma A: " << a << '\n';
    double b = media(turma_b);
    std::cout << "Turma B: " << b << '\n';
    std::cout << "Fim.\n";
}
#include <iostream>
#include <stdexcept>
#include <vector>

double media(const std::vector<double>& notas) {
    if (notas.empty()) {
        throw std::invalid_argument("lista vazia");
    }
    double soma = 0.0;
    for (double n : notas) {
        soma += n;
    }
    return soma / notas.size();
}

int main() {
    std::vector<double> turma_a = {8.5, 6.0, 9.5};
    std::vector<double> turma_b = {};

    try {
        double a = media(turma_a);
        std::cout << "Turma A: " << a << '\n';
        double b = media(turma_b);
        std::cout << "Turma B: " << b << '\n';
    } catch (const std::invalid_argument& e) {
        std::cout << "Erro: " << e.what() << '\n';
    }
    std::cout << "Fim.\n";
}

3. A família std::exception

Você não precisa inventar tipos de erro: a biblioteca padrão traz uma família pronta. Todas descendem de std::exception, a "mãe" de todas, e cada filha diz que tipo de problema aconteceu:

std::exception                  « a mãe de todas (<exception>)
├── std::logic_error            « erro de lógica: o programa pediu algo sem sentido
│   ├── std::invalid_argument   « argumento inválido: stoi("abc"), divisão por zero
│   ├── std::out_of_range       « fora da faixa: v.at(10) num vetor de 3, número grande demais
│   └── std::length_error       « tamanho grande demais para um container
├── std::runtime_error          « erro "do mundo lá fora", só detectável rodando
│   └── std::filesystem::filesystem_error   « (neta, via std::system_error) arquivo não existe…
└── std::bad_alloc              « acabou a memória (new falhou)

A biblioteca padrão já lança várias delas. Você viu algumas em aulas anteriores:

#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>

int main() {
    std::vector<int> v = {10, 20, 30};

    try {
        std::cout << std::stoi("42") << '\n';
        std::cout << std::stoi("quarenta") << '\n';
    } catch (const std::invalid_argument& e) {
        std::cout << "invalid_argument: " << e.what() << '\n';
    }

    try {
        std::cout << std::stoi("99999999999") << '\n';
    } catch (const std::out_of_range& e) {
        std::cout << "out_of_range: " << e.what() << '\n';
    }

    try {
        std::cout << v.at(10) << '\n';
    } catch (const std::out_of_range& e) {
        std::cout << "out_of_range: " << e.what() << '\n';
    }
}
$ g++ -std=c++23 -Wall -Wextra biblioteca.cpp -o biblioteca
$ ./biblioteca
42
invalid_argument: stoi
out_of_range: stoi
out_of_range: vector::_M_range_check: __n (which is 10) >= this->size() (which is 3)
Quem lançaQuandoExceçãowhat() no g++
std::stoi("quarenta")O texto não começa com númerostd::invalid_argumentstoi
std::stoi("99999999999")O número não cabe num intstd::out_of_rangestoi
v.at(10)Posição que não existe (veja std::vector)std::out_of_rangevector::_M_range_check: …
std::filesystem::file_sizeArquivo inexistente (veja Arquivos)filesystem_errorfilesystem error: cannot get file size: …

As mensagens do what() da biblioteca são curtas e técnicas (no g++, o stoi só diz o próprio nome; no editor do guia, com a libc++, ele diz stoi: no conversion, "nenhuma conversão"). Elas servem para o programador. Para o usuário, mostre uma mensagem sua, em português, como nos exemplos abaixo.

Qual lançar nas suas funções

  • std::invalid_argument: quem chamou passou um valor que a função não aceita (divisor zero, idade negativa).
  • std::out_of_range: um número ou posição fora dos limites permitidos.
  • std::runtime_error: algo deu errado "lá fora", e não por culpa de quem chamou (arquivo corrompido, servidor fora do ar). Na dúvida, é esta.

4. Pegar por const&

Em todo catch desta aula, a exceção é pega por referência constante: catch (const std::exception& e). Os motivos:

  • & (referência): você recebe a exceção original, sem cópia, e com o tipo verdadeiro dela. Quem pega uma std::exception por referência e recebe uma runtime_error continua vendo a mensagem certa.
  • const: tratar o erro não deve alterá-lo.

Veja o que acontece ao pegar por valor (sem o &):

#include <iostream>
#include <stdexcept>

int main() {
    try {
        throw std::runtime_error("o servidor não respondeu");
    } catch (std::exception e) {
        std::cout << e.what() << '\n';
    }
}
$ g++ -std=c++23 -Wall -Wextra porvalor.cpp -o porvalor
porvalor.cpp: In function ‘int main()’:
porvalor.cpp:7:29: warning: catching polymorphic type ‘class std::exception’ by value [-Wcatch-value=]
    7 |     } catch (std::exception e) {
      |                             ^
$ ./porvalor
std::exception                « a mensagem "o servidor não respondeu" se perdeu!

O g++ avisou: catching polymorphic type … by value = "pegando um tipo polimórfico por valor". Ao copiar a runtime_error para uma variável do tipo std::exception, só a "parte mãe" foi copiada, e a mensagem ficou para trás. Esse corte tem nome: fatiamento (slicing; você vai entendê-lo em Polimorfismo). Com const std::exception& e, o programa mostraria o servidor não respondeu.


5. Vários catch

Um try pode ter vários catch, um para cada tipo de problema. O C++ testa de cima para baixo e usa o primeiro que servir:

#include <iostream>
#include <stdexcept>
#include <string>

int ler_idade(const std::string& texto) {
    int idade = std::stoi(texto);
    if (idade < 0 || idade > 130) {
        throw std::out_of_range("idade fora da faixa: " + texto);
    }
    return idade;
}

int main() {
    for (std::string texto : {"29", "abc", "200", "99999999999"}) {
        try {
            int idade = ler_idade(texto);
            std::cout << texto << " -> ok, " << idade << " anos\n";
        } catch (const std::invalid_argument& e) {
            std::cout << texto << " -> não é número (" << e.what() << ")\n";
        } catch (const std::out_of_range& e) {
            std::cout << texto << " -> fora da faixa (" << e.what() << ")\n";
        }
    }
}
$ g++ -std=c++23 -Wall -Wextra varios.cpp -o varios
$ ./varios
29 -> ok, 29 anos
abc -> não é número (stoi)
200 -> fora da faixa (idade fora da faixa: 200)
99999999999 -> fora da faixa (stoi)

Repare que o out_of_range veio de dois lugares: do throw da própria ler_idade (idade 200) e de dentro do std::stoi (número enorme). O catch não se importa com quem lançou, só com o tipo.

Pegar qualquer filha: catch (const std::exception& e)

Um catch da mãe pega todas as filhas. E catch (...), com três pontos de verdade, pega qualquer coisa, até o que não é exceção da família (em C++ dá para lançar um int, embora você não deva):

#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>

void tarefa(int n) {
    std::vector<int> v = {1, 2, 3};
    if (n == 1) std::stoi("x");
    if (n == 2) std::cout << v.at(5);
    if (n == 3) throw std::runtime_error("o servidor não respondeu");
    if (n == 4) throw 42;
}

int main() {
    for (int n = 1; n <= 4; ++n) {
        try {
            tarefa(n);
        } catch (const std::exception& e) {
            std::cout << n << ": " << e.what() << '\n';
        } catch (...) {
            std::cout << n << ": erro desconhecido\n";
        }
    }
}
$ g++ -std=c++23 -Wall -Wextra geral.cpp -o geral
$ ./geral
1: stoi
2: vector::_M_range_check: __n (which is 5) >= this->size() (which is 3)
3: o servidor não respondeu
4: erro desconhecido          « o throw 42 não é std::exception: caiu no catch (...)

No catch (...) não existe variável e: você não sabe o que foi lançado, então não há what(). Use-o como última rede de segurança, por exemplo no main, para mostrar uma mensagem amigável em vez de abortar.

A ordem importa: do mais específico para o mais geral

Como vale o primeiro catch que serve, a mãe antes da filha "rouba" tudo:

#include <iostream>
#include <stdexcept>

int main() {
    try {
        throw std::out_of_range("posição 10");
    } catch (const std::exception& e) {
        std::cout << "geral: " << e.what() << '\n';
    } catch (const std::out_of_range& e) {
        std::cout << "específico: " << e.what() << '\n';
    }
}
$ g++ -std=c++23 -Wall -Wextra ordem.cpp -o ordem
ordem.cpp: In function ‘int main()’:
ordem.cpp:9:7: warning: exception of type ‘std::out_of_range’ will be caught by earlier handler [-Wexceptions]
    9 |     } catch (const std::out_of_range& e) {
      |       ^~~~~
ordem.cpp:7:7: note: for type ‘std::exception’
    7 |     } catch (const std::exception& e) {
      |       ^~~~~
$ ./ordem
geral: posição 10             « o catch específico nunca roda

will be caught by earlier handler = "será pega por um tratador anterior". Ponha os catch das filhas primeiro e o da mãe (e o ...) por último.


6. A sua própria exceção

Num programa de banco, "saldo insuficiente" é um erro tão importante que merece um tipo próprio: assim, quem chama pode tratá-lo de um jeito especial e deixar os outros erros para um catch geral. O jeito mais simples de criar uma exceção é fazer uma filha de std::runtime_error:

#include <iostream>
#include <stdexcept>
#include <string>

class SaldoInsuficiente : public std::runtime_error {
public:
    using std::runtime_error::runtime_error;
};

struct Conta {
    std::string dono;
    double saldo = 0.0;
};

void sacar(Conta& c, double valor) {
    if (valor <= 0) {
        throw std::invalid_argument("valor do saque deve ser positivo");
    }
    if (valor > c.saldo) {
        throw SaldoInsuficiente("saldo de " + c.dono + " não cobre o saque");
    }
    c.saldo -= valor;
}

int main() {
    Conta ana{"Ana", 100.0};
    for (double valor : {30.0, 500.0, -5.0, 70.0}) {
        try {
            sacar(ana, valor);
            std::cout << "Sacou " << valor << ", saldo " << ana.saldo << '\n';
        } catch (const SaldoInsuficiente& e) {
            std::cout << "Recusado: " << e.what() << '\n';
        } catch (const std::exception& e) {
            std::cout << "Erro: " << e.what() << '\n';
        }
    }
}
$ g++ -std=c++23 -Wall -Wextra saldo.cpp -o saldo
$ ./saldo
Sacou 30, saldo 70
Recusado: saldo de Ana não cobre o saque
Erro: valor do saque deve ser positivo
Sacou 70, saldo 0
class SaldoInsuficiente : public std::runtime_error
Cria uma classe nova que herda tudo de std::runtime_error: ela é uma runtime_error, e portanto também uma std::exception. Classes e herança são assunto do Nível 3 (Classes e Objetos, Herança); por enquanto, copie este modelo.
public: using std::runtime_error::runtime_error;
"Use os construtores da mãe": é o que permite escrever SaldoInsuficiente("mensagem"), igual a std::runtime_error("mensagem").
throw SaldoInsuficiente("saldo de " + c.dono + " não cobre o saque");
Lança como qualquer outra. A mensagem pode ser montada com +.
catch (const SaldoInsuficiente& e)
Trata só o saldo insuficiente. Os outros erros (como o invalid_argument do valor negativo) caem no catch geral de baixo.

Exceção ou if?

Exceções não substituem o if. Use cada um no seu lugar:

SituaçãoUsePor quê
O problema é esperado e tratado ali mesmo (o usuário digitou uma opção inválida no menu)ifMais simples e mais rápido. Veja Validar a Entrada.
A função não tem como cumprir o que prometeu, e quem sabe tratar está longe (dividir, sacar, stoi)throwA exceção leva o problema até quem sabe agir, e não pode ser ignorada sem querer.
Um "não achei" comum (procurar um nome na agenda)Valor de retorno (bool, std::optional)Não é um erro, é uma resposta possível. Veja optional e variant.

Quando dá errado

"terminate called after throwing an instance of …" e código 134

O que aconteceu: uma exceção foi lançada e nenhum catch a pegou. O tipo vem entre aspas ('std::invalid_argument', 'std::out_of_range'…) e a mensagem, depois de what():.

Como resolver: descubra quem lança (o what() costuma dizer: stoi, vector::_M_range_check) e coloque a chamada dentro de um try com o catch do tipo mostrado. Para achar a linha exata, use o GDB.

"catching polymorphic type … by value"

O que aconteceu: o catch não tem &. A exceção é copiada e pode perder a mensagem.

Como resolver: catch (const std::exception& e).

"exception of type … will be caught by earlier handler"

O que aconteceu: um catch mais geral (a mãe) está antes de um mais específico (a filha), que nunca vai rodar.

Como resolver: inverta a ordem: filhas primeiro, mãe por último.

"‘invalid_argument’ is not a member of ‘std’"
$ g++ -std=c++23 -Wall -Wextra seminclude2.cpp -o seminclude2
seminclude2.cpp: In function ‘int main()’:
seminclude2.cpp:2:16: error: ‘invalid_argument’ is not a member of ‘std’
    2 |     throw std::invalid_argument("teste");
      |                ^~~~~~~~~~~~~~~~
seminclude2.cpp:1:1: note: ‘std::invalid_argument’ is defined in header ‘<stdexcept>’; this is probably fixable by adding ‘#include <stdexcept>’
  +++ |+#include <stdexcept>
    1 | int main() {

O que aconteceu: faltou o #include <stdexcept>, e o g++ até diz qual (is defined in header ‘<stdexcept>’). Com #include <iostream> no arquivo, o mesmo código compila no g++ 15, porque o <iostream> traz o <stdexcept> "de carona". Não conte com isso: outro compilador, ou outra versão, pode não trazer.

Como resolver: inclua <stdexcept> sempre que usar as exceções padrão.


Referência rápida

EscritaSignificado
throw std::runtime_error("msg");Lança uma exceção; a função para ali.
try { … }Bloco vigiado.
catch (const std::out_of_range& e) { … }Trata um tipo específico (e as filhas dele).
catch (const std::exception& e) { … }Trata qualquer exceção padrão.
catch (...) { … }Trata qualquer coisa lançada (sem e).
e.what()A mensagem da exceção.
class X : public std::runtime_error { public: using std::runtime_error::runtime_error; };Uma exceção própria.
#include <stdexcept>runtime_error, invalid_argument, out_of_range, logic_error…
Código de saída 134O programa foi abortado (por exemplo, exceção não tratada).

Exercícios

1. Lançar

Complete para lançar um "argumento inválido" com a mensagem idade negativa:

if (idade < 0) { }

2. Pegar qualquer exceção padrão

Complete o catch que pega qualquer filha de std::exception, do jeito recomendado, com o nome e:

catch () { std::cout << e.what(); }

3. A mensagem

Dentro do catch, qual chamada devolve a mensagem da exceção e?

4. O que mostra este trecho? try { std::cout << "A"; throw std::runtime_error("x"); std::cout << "B"; } catch (const std::exception& e) { std::cout << "C"; } std::cout << "D";

5. O programa terminou com terminate called after throwing an instance of 'std::out_of_range' e código 134. O que aconteceu?

6. Em que ordem devem vir os catch?

Missão final: estoque insuficiente

Missão: a lojinha está vendendo cadernos que não tem (rode e veja o estoque ficar negativo). Crie a classe EstoqueInsuficiente, filha de std::runtime_error, e faça vender lançá-la com a mensagem "só restam " + std::to_string(p.estoque) quando a quantidade pedida passar do estoque. A segunda venda deve mostrar Não deu: só restam 2. Se quiser, troque o catch para pegar só a sua exceção.

#include <iostream>
#include <stdexcept>
#include <string>

// crie aqui a classe EstoqueInsuficiente, filha de std::runtime_error

struct Produto {
    std::string nome;
    int estoque = 0;
};

void vender(Produto& p, int qtd) {
    // se qtd for maior que p.estoque, lance EstoqueInsuficiente
    p.estoque -= qtd;
}

int main() {
    Produto caderno{"caderno", 5};
    for (int qtd : {3, 10}) {
        try {
            vender(caderno, qtd);
            std::cout << "Vendidos " << qtd << ", restam " << caderno.estoque << '\n';
        } catch (const std::exception& e) {
            std::cout << "Não deu: " << e.what() << '\n';
        }
    }
}
#include <iostream>
#include <stdexcept>
#include <string>

class EstoqueInsuficiente : public std::runtime_error {
public:
    using std::runtime_error::runtime_error;
};

struct Produto {
    std::string nome;
    int estoque = 0;
};

void vender(Produto& p, int qtd) {
    if (qtd > p.estoque) {
        throw EstoqueInsuficiente("só restam " + std::to_string(p.estoque));
    }
    p.estoque -= qtd;
}

int main() {
    Produto caderno{"caderno", 5};
    for (int qtd : {3, 10}) {
        try {
            vender(caderno, qtd);
            std::cout << "Vendidos " << qtd << ", restam " << caderno.estoque << '\n';
        } catch (const EstoqueInsuficiente& e) {
            std::cout << "Não deu: " << e.what() << '\n';
        }
    }
}

Resumo

  • throw lança uma exceção: a função para e o erro "sobe" até um try.
  • try { … } catch (const Tipo& e) { … } trata o erro, e o programa continua depois do catch.
  • Sem catch: terminate called after throwing an instance of …, what(): …, "Abortado" e código 134.
  • Família padrão (<stdexcept>): std::exception é a mãe; use invalid_argument, out_of_range e runtime_error.
  • Pegue sempre por const&; leia a mensagem com e.what().
  • Vários catch: das filhas para a mãe; catch (...) por último.
  • Exceção própria: class X : public std::runtime_error { public: using std::runtime_error::runtime_error; };.