CodeGym /Cursos /C++ SELF /=default — geramos operações como parte da API

=default — geramos operações como parte da API

C++ SELF
Nível 45, Lição 0
Disponível

1. Por que usar =default se "já funciona"?

Quando você começa a escrever seus struct, parece que a linguagem já cuida de tudo: campos são copiados, strings não vazam, vetores liberam memória sozinhos, a vida é boa. Mas então você adiciona uma "pequena melhoria" — por exemplo, um construtor com parâmetro — e de repente descobre que T t; não compila mais. Ou você quer que qualquer leitor do código veja: "sim, esse tipo pode ser copiado, e a cópia é a padrão". É aí que =default vira sua maneira de se comunicar com o compilador e com os colegas na mesma linguagem.

=default não é "faça uma função vazia". É um comando: "gere a implementação padrão dessa função como você faria automaticamente". Ou seja, você não inventa a lógica — você confirma que a lógica padrão serve e a fixa na API do tipo.

Na prática, =default é mais usado para funções-membro especiais:

Função Do que se trata Quando é chamada
T() = default;
construtor padrão T x;, T x{}; (em diferentes contextos)
~T() = default;
destrutor no término do tempo de vida do objeto
T(const T&) = default;
construtor de cópia T b = a;
T& operator=(const T&) = default;
atribuição por cópia b = a;

(Às vezes adicionam também as operações de "move", mas hoje vamos propositalmente pular esse assunto para não avançar para o próximo encontro.)

2. Sintaxe de =default e a ideia de "contrato"

Em programação é conveniente ter uma "notação curta" que diga: "faça certo e padrão". Em C++ isso é exatamente =default. Psicologicamente é parecido com o fato de você não escrever seu próprio algoritmo de ordenação por bolha e confiar em std::sort. Aqui o papel de std::sort é desempenhado pelo compilador.

A sintaxe é igualmente simples para todas as funções especiais:


struct T {
    T() = default;
    ~T() = default;
    T(const T&) = default;
    T& operator=(const T&) = default;
};

Aqui é importante não confundir duas coisas.

Primeiro, =default não é colocar chaves vazias. Um corpo vazio ({}) significa: "eu escrevi minha função, ela é assim". E =default significa: "gere a versão padrão, como você sabe fazer". Às vezes a diferença só aparece em propriedades sutis do tipo, mas para o desenho da interface é fundamental: =default é um marcador de intenção.

Segundo, =default não "quebra as leis da física". Se a versão padrão não pode existir por causa da composição dos campos (por exemplo, você tem um campo non-copyable), então =default não se transforma numa varinha mágica. No máximo — ele lhe dá um contrato mais direto e honesto no código: "queríamos comportamento padrão, mas ele não está disponível".

3. Como restaurar o construtor padrão

A razão mais comum para aprender =default não nos livros, mas na prática, é a situação "eu adicionei um construtor e tudo quebrou". Em C++ existe a regra: se você declarou qualquer construtor personalizado, o compilador não é mais obrigado a fornecer o construtor padrão automaticamente.

Vamos encaixar isso em nossa mini-aplicação didática. Suponha que estamos escrevendo um gerenciador simples de tarefas de console TaskBox, e temos as configurações do aplicativo.

Primeiro — a versão "ingênua":

#include <string>

struct AppConfig {
    std::string storagePath = "tasks.txt";
};

Essa versão é criada tranquilamente como AppConfig cfg{}; e vive em paz.

Agora queremos permitir especificar o caminho explicitamente:

#include <string>

struct AppConfig {
    std::string storagePath = "tasks.txt";

    explicit AppConfig(const std::string& path) : storagePath(path) {}
};

E aí o iniciante frequentemente se pergunta: "Por que AppConfig cfg; virou um problema?" Resposta: porque mexemos no modelo de criação do objeto. E se ainda precisamos de um objeto "padrão", devemos dizer isso ao compilador explicitamente — da forma mais honesta:

#include <string>

struct AppConfig {
    std::string storagePath = "tasks.txt";

    AppConfig() = default; // restaurando a criação "padrão"
    explicit AppConfig(const std::string& path) : storagePath(path) {}
};

Agora ambos os cenários são legais novamente: tanto "execute com padrões" quanto "execute com configuração".

Vamos animar isso no main, para parecer um pedaço de aplicativo:

#include <iostream>
#include <string>

struct AppConfig {
    std::string storagePath = "tasks.txt";

    AppConfig() = default;
    explicit AppConfig(const std::string& path) : storagePath(path) {}
};

int main() {
    AppConfig a{};
    AppConfig b{"backup_tasks.txt"};

    std::cout << a.storagePath << '\n'; // tasks.txt
    std::cout << b.storagePath << '\n'; // backup_tasks.txt
}

O sentido de =default aqui é muito prático: nós não escrevemos manualmente a "inicialização padrão", pedimos ao compilador para fazê-la de forma padrão (respeitando inicializadores de campo, ordem dos campos etc.).

4. Cópia e diferença para corpos vazios

Cópia: tornar o comportamento padrão parte da interface

Outra razão para usar =default — não porque sem ele o código não compila, mas porque sem ele não fica tão explícito. Às vezes é importante que o usuário do tipo veja: "a cópia é permitida, e ela é a padrão, campo a campo".

Em nosso TaskBox vamos ter um modelo de tarefa:

#include <string>

struct Task {
    int id{};
    std::string title;

    Task() = default;
    Task(const Task&) = default;
    Task& operator=(const Task&) = default;
};

Tecnicamente para esse Task poderíamos não escrever nada: o compilador geraria tudo. Mas aqui o foco é a ideia: =default é uma forma de tornar o comportamento explícito.

Vejamos a cópia em ação:

#include <iostream>
#include <string>

struct Task {
    int id{};
    std::string title;

    Task() = default;
    Task(const Task&) = default;
    Task& operator=(const Task&) = default;
};

int main() {
    Task a{1, "Read C++ book"};
    Task b = a;            // construtor de cópia
    b.title = "Write C++ code";

    std::cout << a.title << '\n'; // Read C++ book
    std::cout << b.title << '\n'; // Write C++ code
}

Por que isso é didaticamente importante? Porque você passa a ver as funções especiais não como "mágica do compilador", mas como parte do contrato público. E quando encontrar um tipo que não pode ser copiado, você lerá o código na ótica correta: "ah, a cópia é proibida pelo modelo".

Por que T() {} não é o mesmo que T() = default;

Nesse ponto normalmente surge: "Ok, e se eu escrever um construtor vazio, isso não é quase o mesmo que =default?"

Se você escreve:

struct X {
    X() {}
};

você cria um construtor personalizado. O compilador não o considera mais um comportamento "padrão".

E quando você escreve:

struct X {
    X() = default;
};

você diz: "gere a implementação padrão". Ou seja, em um caso você afirma: "minha lógica é vazia", e no outro: "a lógica é a padrão".

O efeito prático para um iniciante costuma ser: um construtor vazio é um lugar onde você começa a acrescentar "um pouquinho de lógica" sem perceber, e depois de um tempo se pergunta por que o objeto pode ficar em um estado estranho. =default funciona psicologicamente como um cadeado na porta: se você queria "criação padrão", você a fixou.

Outra sutileza (apenas como fato, sem entrar em detalhes): operações geradas pelo padrão às vezes têm propriedades adicionais úteis para otimizações e regras estritas da linguagem. Uma função personalizada, mesmo vazia, pode perder essas propriedades. Se você não quer alterar acidentalmente o "caráter" do tipo, =default é a escolha mais segura.

5. Onde escrever =default: no tipo ou em .cpp

Quando o projeto cresce um pouco, vem o instinto de separar declarações e definições em arquivos: em .hpp declara, em .cpp define. Isso é normal — aparece no fluxo de quem começa a lidar com o linker.

Mas há um detalhe com =default: o lugar onde você o escreve pode mudar o status da função. De forma prática, a regra é: na maioria dos casos é melhor escrever =default exatamente onde você declara a função — ou seja, na definição do struct.

Por exemplo, se temos AppConfig, normalmente é melhor assim:

// config.hpp
#pragma once
#include <string>

struct AppConfig {
    std::string storagePath = "tasks.txt";
    AppConfig() = default;
};

E este outro modo — aceitável, mas exige cuidado:

// config.hpp
#pragma once
#include <string>

struct AppConfig {
    std::string storagePath = "tasks.txt";
    AppConfig(); // apenas declaração
};
// config.cpp
#include "config.hpp"
AppConfig::AppConfig() = default;

Do ponto de vista "vai compilar" — ambos normalmente funcionam. Mas do ponto de vista "eu queria o comportamento mais padrão possível sem efeitos colaterais", a segunda opção às vezes torna a função mais "personalizada" do que você espera. Por isso o tipo pode se tornar "menos simples" do que poderia ser.

Lembre-se: =default é sobre intenção. E intenções são melhores quando escritas onde serão vistas à primeira vista, não num distante .cpp que só se olha quando algo deu errado.

6. Exemplo prático: TaskBox e =default

Para consolidar, vamos montar uma mini-versão da nossa aplicação TaskBox: há um config, há uma task, há um "armazenamento de tarefas" em memória. Não fazemos I/O em arquivo nem comandos complexos — o importante é ver como =default forma um contrato claro.

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

struct AppConfig {
    std::string storagePath = "tasks.txt";
    AppConfig() = default;
    explicit AppConfig(const std::string& path) : storagePath(path) {}
};

struct Task {
    int id{};
    std::string title;

    Task() = default;
    Task(const Task&) = default;
    Task& operator=(const Task&) = default;
};

struct TaskStore {
    std::vector<Task> tasks;

    TaskStore() = default;
    TaskStore(const TaskStore&) = default;
    TaskStore& operator=(const TaskStore&) = default;
};

int main() {
    AppConfig cfg{};
    TaskStore store{};

    store.tasks.push_back(Task{1, "Buy milk"});
    TaskStore backup = store;

    std::cout << cfg.storagePath << '\n';        // tasks.txt
    std::cout << backup.tasks[0].title << '\n';  // Buy milk
}

Note a sensação do código: ele "não tenta ser mais esperto que o necessário". Não escrevemos cópia manual, não tentamos "otimizar visualmente", não adicionamos ações suspeitas no destrutor. Mas tornamos o contrato explícito: esses tipos são criados de forma padrão, copiados de forma padrão, e os recursos são gerenciados pelos próprios campos (std::string, std::vector).

7. Erros típicos ao trabalhar com =default

Erro nº 1: escrever um corpo vazio {} em vez de =default "porque parecem iguais".
No nível "compila" pode parecer que T() {} e T() =default; são gêmeos. Mas o significado é diferente: corpo vazio é sua implementação, e =default é um pedido ao compilador para gerar a versão padrão. Por essa diferença, o tipo pode começar a se comportar "um pouco diferente" em termos formais da linguagem, e, principalmente, perde-se clareza de intenção.

Erro nº 2: esperar que =default "restaure" uma operação proibida por causa dos campos.
Se dentro do tipo há um campo que não é copiável (por exemplo, std::unique_ptr), então a cópia do tipo contendo esse campo não estará disponível por causa do modelo de propriedade. Nessa situação escrever T(const T&) =default; não torna a cópia possível; apenas fixa honestamente sua intenção — o compilador ainda não conseguirá gerar a cópia corretamente.

Erro nº 3: adicionar um construtor com parâmetro e esquecer que o construtor padrão pode desaparecer.
Essa é uma armadilha clássica: você melhorou a API (Config(path)) e depois se surpreendeu por que Config cfg; não compilar mais. Se um "objeto default" ainda faz sentido, você precisa restaurá-lo explicitamente: Config() =default;. Esse é um dos usos mais úteis e práticos de =default.

Erro nº 4: declarar uma função especial no cabeçalho e escrever =default no .cpp, sem entender que isso muda o status da função.
Isso é possível, e às vezes justificado, mas então você deve aceitar conscientemente as consequências: a função pode deixar de ser "o mais padrão possível" em termos formais da linguagem. Se você não pretende mudar o caráter do tipo, normalmente é mais simples e honesto escrever =default diretamente na definição do struct.

1
Tarefa
C++ SELF, nível 45, lição 0
Bloqueado
Configuração do servidor
Configuração do servidor
1
Tarefa
C++ SELF, nível 45, lição 0
Bloqueado
Rótulo da tarefa
Rótulo da tarefa
1
Tarefa
C++ SELF, nível 45, lição 0
Bloqueado
Lista de compras
Lista de compras
1
Tarefa
C++ SELF, nível 45, lição 0
Bloqueado
Perfil do jogador
Perfil do jogador
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION