1. Por que o C++ precisa de noexcept
Se você está acostumado com o habitual return, então “sair por exceção” soa como uma realidade alternativa. E, em certo sentido, é isso: é um caminho em que a função não devolve o controle da forma habitual, e a execução “vaza” para fora. Hoje não vamos estudar a sintaxe de try/catch nem aprender a “capturar” exceções — nos basta o modelo: às vezes dentro de uma função pode ocorrer um evento que faz o controle deixar a função não por meio de return.
noexcept é uma forma de dizer ao compilador e a todos os leitores do código: “esta função nunca deve terminar com essa saída abrupta via exceção”. Isso não é um comentário nem um pedido. É uma promessa no nível de “contrato”. Além disso, nas versões modernas do C++ a especificação de exceções faz parte do tipo da função (isto é, afeta compatibilidade de assinaturas e overload).
E aqui aparece o ponto principal: noexcept ajuda a tornar o código previsível. Quando você projeta um tipo com movimentação, quer que o move seja “à prova de balas”: transferir o ponteiro, zerar a origem — e pronto. Sem surpresas.
Sintaxe de noexcept
Por fora noexcept parece quase simples demais — e isso é bom. Um contrato deve ser lido instantaneamente.
Forma mínima:
#include <iostream>
void print_hello() noexcept {
std::cout << "Hello!\n"; // Hello!
}
Aqui noexcept aparece na declaração da função e significa: essa função não deve terminar saindo por exceção. No nosso modelo simplificado hoje pode-se pensar assim: se dentro de print_hello() acontecer algo que tente “sair para fora como exceção”, o programa não continuará “como de costume”.
Outro ponto importante: noexcept não é um “acelerador”. Sim, ele pode influenciar otimizações, mas antes de tudo é semântica. Um contrato. Uma promessa.
Às vezes você verá a forma noexcept(true) ou noexcept(false). Por enquanto basta entender a ideia: noexcept pode ser condicional, mas nos nossos exemplos usaremos a forma básica noexcept, porque ela é a mais adequada para operações de move em tipos com recurso.
2. O que acontece quando se viola noexcept
Imagine que noexcept é uma placa “NÃO ENTRAR” na porta. Enquanto você respeitá‑la — tudo bem. Mas se você entrar, não é um “multa”, é “a porta se fecha atrás de você e o prédio entra em modo de evacuação”.
Tecnicamente isso se expressa assim: se uma exceção tentar escapar de uma função marcada com noexcept, é chamada std::terminate() e o programa termina abruptamente. Isso não é “pode acontecer”, é o modelo da linguagem.
Para intuição é útil desenhar um fluxograma:
flowchart TD
A["Função declarada noexcept"] --> B["Algo ocorreu dentro que tenta sair como exceção"]
B --> C{"A exceção sai da função noexcept?"}
C -- "Sim" --> D["std::terminate()"]
D --> E["Programa termina abruptamente"]
C -- "Não" --> F["A execução continua normalmente"]
Por que tão rígido? Porque noexcept é uma promessa que outras partes do programa e a biblioteca padrão podem assumir. Se violar noexcept fosse tratado “suavemente”, todo o sentido do contrato se perderia.
Portanto uma regra importante para iniciantes é: nunca marque noexcept “por precaução”. Não é “vamos colocar só por garantia”, é “eu tenho certeza de que nada vai escapar”.
4. Por que noexcept é importante para operações de move
Aqui frequentemente perguntam: “Ok, o contrato é rígido. Mas por que há tanto papo sobre noexcept em torno do move?” A razão é muito prática: a movimentação é usada dentro de contêineres padrão, especialmente quando o contêiner precisa realocar elementos para um novo local.
Imagine std::vector<T>. Ele guarda elementos em um bloco contínuo de memória. Às vezes precisa aumentar a capacidade, alocar um novo bloco e mover os elementos para lá. Surge a pergunta: como mover os elementos — por cópia ou por move?
Se o move‑construtor do tipo T está marcado noexcept, então para o vector a movimentação é uma “operação segura de realocação”. Se o move puder lançar, o contêiner pode preferir copiar (se a cópia estiver disponível). Na biblioteca padrão existe até a ideia “move se não lançar” — frequentemente associada a move_if_noexcept (é apenas um nome útil, por agora basta captar a motivação).
Isso não significa que sem noexcept tudo quebre. Significa que o tipo fica menos amigável para contêineres e algoritmos que querem reorganizar elementos de forma segura e eficiente.
5. Exemplo: um Buffer que gerencia recurso e operações de move com noexcept
Vamos ver noexcept em um tipo que possui recurso diretamente.
Suponha que temos um Buffer — dono de um array dinâmico de int. É um exemplo didático, mas ilustra bem a mecânica de propriedade:
#include <cstddef>
struct Buffer {
std::size_t size{};
int* data{nullptr};
explicit Buffer(std::size_t n) : size(n), data(new int[n]{}) {}
~Buffer() { delete[] data; }
};
O problema já é familiar: a cópia padrão quebraria a propriedade (copiaria o endereço). Portanto em aulas anteriores adicionamos cópia profunda (construtor de cópia e operador de cópia). Agora vamos adicionar move — e é justamente aí que noexcept costuma ser apropriado.
Move-construtor e noexcept
É importante explicitar o sentido: o move‑construtor normalmente faz coisas bem simples: copiar o ponteiro e o tamanho, zerar a fonte. Em tal código geralmente não alocamos memória nem fazemos operações “perigosas”. Por isso é razoável e lógico declará‑lo noexcept: podemos honestamente prometer que nada escapará.
#include <cstddef>
struct Buffer {
std::size_t size{};
int* data{nullptr};
explicit Buffer(std::size_t n) : size(n), data(new int[n]{}) {}
~Buffer() { delete[] data; }
Buffer(Buffer&& other) noexcept : size(other.size), data(other.data) {
other.size = 0;
other.data = nullptr;
}
};
Repare nos dois “atos sagrados”: tomamos o recurso e deixamos a origem em estado vazio. Esse estado vazio será corretamente tratado pelo destrutor (delete[] de nullptr é seguro).
Operador de atribuição por move e noexcept
A lógica é parecida, mas surge responsabilidade adicional: o lado esquerdo já pode ter um recurso que precisa ser liberado cuidadosamente. Aqui também geralmente não há motivos para lançar, se você não faz nada além de delete[] e atribuições.
#include <cstddef>
struct Buffer {
std::size_t size{};
int* data{nullptr};
explicit Buffer(std::size_t n) : size(n), data(new int[n]{}) {}
~Buffer() { delete[] data; }
Buffer(Buffer&& other) noexcept : size(other.size), data(other.data) {
other.size = 0;
other.data = nullptr;
}
Buffer& operator=(Buffer&& other) noexcept {
if (this == &other) return *this;
delete[] data;
size = other.size;
data = other.data;
other.size = 0;
other.data = nullptr;
return *this;
}
};
Uma “checagem de bom senso” pequena: se nas operações de move você de repente usa new, provavelmente está fazendo algo errado. Move é “transferência de propriedade”, não “construção de um novo recurso”.
6. Como decidir se é possível escrever noexcept
Iniciantes querem uma receita universal: “Diga quando pode e quando não pode”. Não existe uma regra 100% universal. Mas há uma lógica prática de verificação que funciona mesmo sem conhecimento profundo de try/catch.
Primeiro pergunte: “Esta função pode, dentro de si, fazer algo que potencialmente leve a uma tentativa de sair por exceção?” Suspeitos típicos — alocação de memória, criação de objetos complexos, operações de I/O, contêineres “inteligentes” que alocam internamente. Se a resposta for “sim” ou “não tenho certeza” — não marque noexcept.
Se a função faz apenas ações simples e previsíveis, como atribuição de ponteiros, troca de inteiros, zerar campos, liberação de recurso, então noexcept geralmente é apropriado.
É útil ter uma mini‑tabela:
| Operação | Geralmente adequado noexcept? | Por quê |
|---|---|---|
| Move “transferir ponteiro + zerar” | Sim | Normalmente não há alocações |
| Destrutor que apenas libera recurso | Sim | Liberação do recurso deve ser confiável |
| Cópia com new e loop de cópia | Normalmente não | A alocação de memória pode falhar |
| Função que escreve em std::cout | Normalmente não | I/O pode lançar, e é melhor não prometer “aço” |
noexcept é parte da interface. Você não está apenas dizendo “por dentro está tudo ok”, você está dizendo ao usuário do tipo: “pode contar comigo”. Portanto é melhor prometer de menos do que prometer demais.
Por que noexcept é sobre confiança
Em programação, “promessas” não existem por moralidade, mas para que outras partes do sistema possam tomar decisões. E noexcept é exatamente essa promessa. Ele permite escrever código mais simples e previsível em torno da sua função.
Por exemplo, se o seu tipo tem operações de move marcadas com noexcept, então mover pode ser usado como um bloco básico para operações seguras de rearranjo. Mesmo que você ainda não use isso diretamente, a biblioteca padrão pode usar.
Além disso, noexcept disciplina o autor. Se você marcou o move como noexcept, passa a ter a regra interna: “no move eu não adiciono lógica que possa criar problemas”. Isso frequentemente leva a um design mais limpo: move permanece “transferência mecânica de propriedade”, e toda lógica complexa vai para funções normais ou para a cópia, se esta for necessária.
7. Erros típicos ao trabalhar com noexcept
Erro nº 1: marcar noexcept porque “é o que se faz”.
A armadilha mais comum é ver na internet Buffer(Buffer&&) noexcept e copiar isso sem entender o custo da promessa. Se dentro da função existe ao menos um cenário em que uma saída por exceção é possível, você está assinando um contrato que, se violado, encerrará o programa por std::terminate().
Erro nº 2: marcar noexcept em uma função que aloca memória.
O iniciante às vezes pensa: “mas normalmente a alocação funciona, então tudo bem”. Mas “normalmente” não faz parte do contrato. Alocação é como procurar vaga num estacionamento lotado em véspera de feriado: às vezes dá certo, às vezes não. Logo, cópia que faz new quase nunca deve ser marcada noexcept.
Erro nº 3: confundir noexcept com const.
Essas palavras aparecem juntas em declarações e o cérebro tenta fundi‑las. const diz “o método não altera o estado do objeto”. noexcept diz “a função não terminará saindo por exceção”. São eixos diferentes: um sobre mutação, outro sobre caminho de erro abrupto.
Erro nº 4: complicar as operações de move e esquecer de revisar o noexcept.
História típica: inicialmente move transfere o ponteiro e é honestamente noexcept. Depois alguém adiciona logging, recálculo de hash, criação temporária de std::string, e o move deixa de ser tão simples. Mas o noexcept permanece. O resultado é uma bomba-relógio: tudo parece ok, mas em algum momento o programa pode terminar abruptamente.
Erro nº 5: pensar que noexcept é “sobre otimização, não preciso me preocupar”.
Sim, noexcept pode melhorar desempenho e comportamento de contêineres. Mas seu sentido principal é correção do contrato. Não é um ajuste fino do motor; é o freio. Otimização é um bônus agradável, não a razão para marcar noexcept.
GO TO FULL VERSION