Compartilhar via


Funções explicitamente usadas como padrão e excluídas

No C++11, as funções usadas como padrão e excluídas proporcionam controle explícito sobre se as funções de membro especial serão geradas automaticamente. As funções excluídas também oferecem uma linguagem simples para evitar que promoções de tipo problemático ocorram em argumentos para funções de todos os tipos — funções de membro especial e funções de membro normal e funções de não membro — que, de outra forma, causariam uma chamada de função indesejada.

Benefícios das funções explicitamente usadas como padrão e excluídas

Em C++, o compilador gera automaticamente o construtor padrão, o construtor copy, o operador de atribuição de cópia e o destruidor para um tipo se ele não declarar seu próprio. Essas funções são conhecidas como funções de membro especial e são elas que fazem com que tipos simples definidos pelo usuário em C++ se comportem como estruturas em C. Ou seja, você pode criá-los, copiá-los e destruí-los sem esforço extra de codificação. O C++11 traz a semântica de movimentação para a linguagem e adiciona o construtor de movimentação e o operador de atribuição de movimentação à lista de funções de membro especial que o compilador pode gerar automaticamente.

Isso é conveniente para tipos simples, mas os tipos complexos geralmente definem, por conta própria, uma ou mais das funções de membro especial, e isso pode impedir que outras funções de membro especial sejam geradas automaticamente. Na prática:

  • Se qualquer construtor for declarado explicitamente, nenhum construtor padrão será gerado automaticamente.

  • Se um destruidor virtual for declarado explicitamente, nenhum destruidor padrão será gerado automaticamente.

  • Se um construtor de movimentação ou um operador de atribuição de movimentação for declarado explicitamente:

    • Nenhum construtor de cópia será gerado automaticamente.

    • Nenhum operador de atribuição de cópia será gerado automaticamente.

  • Se um construtor de cópia, um operador de atribuição de cópia, um construtor de movimentação, um operador de atribuição de movimentação ou um destruidor for declarado explicitamente:

    • Nenhum construtor de movimentação será gerado automaticamente.

    • Nenhum operador de atribuição de movimentação será gerado automaticamente.

Observação

Além disso, o padrão C++11 especifica as seguintes regras adicionais:

  • Se um construtor de cópia ou um destruidor for declarado explicitamente, a geração automática do operador de atribuição de cópia será preterida.
  • Se um operador de atribuição de cópia ou um destruidor for declarado explicitamente, a geração automática do construtor de cópia será preterida.

Em ambos os casos, o Visual Studio continua a gerar automaticamente as funções necessárias implicitamente e não emite um aviso por padrão. Desde o Visual Studio 2022 versão 17.7, C5267 pode ser habilitado para emitir um aviso.

As consequências dessas regras também podem vazar para as hierarquias de objetos. Por exemplo, se, por qualquer motivo, uma classe base não conseguir ter um construtor padrão que possa ser chamado de uma classe derivada, ou seja, um public construtor ou protected que não usa parâmetros, uma classe derivada dela não poderá gerar automaticamente seu próprio construtor padrão.

Essas regras podem complicar a implementação do que devem ser tipos diretos definidos pelo usuário e expressões idiomáticas comuns do C++ — por exemplo, tornar um tipo definido pelo usuário não copiável declarando o construtor de cópia e o operador de atribuição de cópia de forma privada e não definindo-os.

struct noncopyable
{
  noncopyable() {};

private:
  noncopyable(const noncopyable&);
  noncopyable& operator=(const noncopyable&);
};

Antes do C++11, esse trecho de código era a forma idiomática de tipos não copiáveis. No entanto, existem vários problemas:

  • O construtor de cópia precisa ser declarado de forma privada para ocultá-lo. Porém, como ele é declarado, a geração automática do construtor padrão é impedida. Você terá que definir o construtor padrão explicitamente se quiser um, mesmo que ele não faça nada.

  • Mesmo que o construtor padrão explicitamente definido não faça nada, o compilador o considera não trivial. É menos eficiente do que um construtor padrão gerado automaticamente e impede que noncopyable seja um tipo POD verdadeiro.

  • Mesmo que o construtor de cópia e o operador de atribuição de cópia estejam ocultos do código externo, as funções de membro e os amigos de noncopyable ainda poderão vê-los e chamá-los. Se eles forem declarados, mas não definidos, chamá-los causará um erro de vinculador.

  • Embora este seja um idioma comumente aceito, a intenção não é clara, a menos que você entenda todas as regras para geração automática das funções de membro especial.

No C++11, o idioma não copiável pode ser implementado de uma maneira mais direta.

struct noncopyable
{
  noncopyable() =default;
  noncopyable(const noncopyable&) =delete;
  noncopyable& operator=(const noncopyable&) =delete;
};

Observe como os problemas com a expressão pré-C++11 são resolvidos:

  • A geração do construtor padrão ainda é impedida por meio da declaração do construtor de cópia, mas você pode trazê-lo de volta ao usá-lo como padrão explicitamente.

  • As funções de membro especial explicitamente padrão ainda são consideradas triviais, portanto, não há penalidade de desempenho e noncopyable não é impedida de ser um tipo POD verdadeiro.

  • O construtor de cópia e o operador de atribuição de cópia são públicos, mas são excluídos. É um erro em tempo de compilação definir ou chamar uma função excluída.

  • A intenção é clara para qualquer pessoa que compreenda =default e =delete. Você não precisa compreender as regras para a geração automática de funções de membro especial.

Existem expressões idiomáticas semelhantes para criar tipos definidos pelo usuário que não são móveis, que só podem ser alocados dinamicamente ou que não podem ser alocados dinamicamente. Cada uma dessas expressões tem implementações pré-C++11 que sofrem problemas semelhantes, e que são resolvidas de forma semelhante no C++11 pela implementação delas em termos de funções de membro especial usadas como padrão e excluídas.

Funções explicitamente usadas como padrão

Você pode padronizar qualquer uma das funções de membro especial — para declarar explicitamente que a função de membro especial usa a implementação padrão, para definir a função de membro especial com um qualificador de acesso não público ou para restabelecer uma função de membro especial cuja geração automática foi impedida por outras circunstâncias.

Para usar uma função de membro especial como padrão, você a declara, como neste exemplo:

struct widget
{
  widget()=default;

  inline widget& operator=(const widget&);
};

inline widget& widget::operator=(const widget&) =default;

Observe que é possível usar uma função de membro especial como padrão fora do corpo de uma classe, desde que ela possa ser usada em linha.

Por causa dos benefícios de desempenho das funções de membro especial triviais, recomendamos que você prefira as funções de membro especial geradas automaticamente aos corpos de função vazios quando quiser o comportamento padrão. Você pode fazer isso usando a função de membro especial como padrão explicitamente, ou não a declarando (e também não declarando outras funções de membro especial que a impediriam de ser gerada automaticamente).

Funções excluídas

Você pode excluir funções de membro especial e funções de membro normal e funções de não membro para impedir que elas sejam definidas ou chamadas. A exclusão de funções de membro especial oferece uma forma mais limpa de impedir que o compilador gere funções de membro especial indesejadas. A função deve ser excluída conforme é declarada; ele não pode ser excluído depois da maneira que uma função pode ser declarada e, em seguida, depois padrão.

struct widget
{
  // deleted operator new prevents widget from being dynamically allocated.
  void* operator new(std::size_t) = delete;
};

A exclusão de funções normais de membro ou de não-membro impede que promoções de tipo problemático façam com que uma função não intencional seja chamada. Isso funciona porque as funções excluídas ainda participam da resolução de sobrecarga e fornecem uma correspondência melhor do que a função que poderia ser chamada após a promoção dos tipos. A chamada de função é resolvida para a função mais específica, mas excluída, e causa um erro do compilador.

// deleted overload prevents call through type promotion of float to double from succeeding.
void call_with_true_double_only(float) =delete;
void call_with_true_double_only(double param) { return; }

Observe no exemplo anterior que chamar usando um argumento causaria um erro do compilador, mas chamar call_with_true_double_only call_with_true_double_only usando um float int argumento não, no int caso, o argumento será promovido de int para double e chamará com êxito a double versão da função, mesmo que isso não seja o que você pretende. Para garantir que qualquer chamada para essa função usando um argumento não duplo cause um erro de compilador, você pode declarar uma versão de modelo da função excluída.

template < typename T >
void call_with_true_double_only(T) =delete; //prevent call through type promotion of any T to double from succeeding.

void call_with_true_double_only(double param) { return; } // also define for const double, double&, etc. as needed.