Enum estranho no destruidor

83

Atualmente, estou lendo o código-fonte de Protocol Buffere encontrei um enumcódigo estranho definido aqui

  ~scoped_ptr() {
    enum { type_must_be_complete = sizeof(C) };
    delete ptr_;
  }

  void reset(C* p = NULL) {
    if (p != ptr_) {
      enum { type_must_be_complete = sizeof(C) };
      delete ptr_;
      ptr_ = p;
    }
  }

Por que o enum { type_must_be_complete = sizeof(C) };está definido aqui? para que isso é usado?

zangw
fonte
2
Se eu quiser ter essa certeza, prefiro usar a ptr_si mesmo no sizeofcomo em sizeof(*ptr_)vez de sizeof(C).
Nawaz de

Respostas:

81

Esse truque evita o UB, garantindo que a definição de C esteja disponível quando este destruidor for compilado. Caso contrário, a compilação falhará porque o sizeoftipo incompleto (tipos declarados para frente) não pode ser determinado, mas os ponteiros podem ser usados.

No binário compilado, esse código seria otimizado e não teria efeito.

Observe que: Excluir o tipo incompleto pode ser um comportamento indefinido de 5.3.5 / 5 :.

se o objeto que está sendo excluído tiver um tipo de classe incompleta no ponto de exclusão e a classe completa tiver um destruidor não trivial ou uma função de desalocação, o comportamento será indefinido .

g++ até emite o seguinte aviso:

aviso: possível problema detectado na invocação do operador de exclusão:
aviso: 'p' tem tipo incompleto
aviso: declaração de encaminhamento de 'struct C'

Mohit Jain
fonte
1
"Excluir o tipo incompleto é um comportamento indefinido" está incorreto. É apenas UB se o tipo tiver um destruidor não trivial. O problema que esse pequeno truque aborda é precisamente que a exclusão de tipo incompleto nem sempre é UB, de modo que a linguagem o suporta.
Saúde e hth. - Alf
Obrigado @ Cheersandhth.-Alf Meu ponto é que pode ser UB, então em geral esta linha de código é UB. Editado.
Mohit Jain de
32

sizeof(C)irá falhar em tempo de compilação se Cnão for um tipo completo. Definir um escopo local enumpara ele torna a instrução benigna em tempo de execução.

É uma forma do programador se proteger de si mesmo: o comportamento de um subsequente delete ptr_em um tipo incompleto é indefinido se ele possuir um destruidor não trivial.

Bate-Seba
fonte
1
Você pode explicar por que C precisa ser um tipo completo nesse ponto - é necessário ter a definição de tipo completa para chamá delete-lo? E em caso afirmativo, por que o compilador não o detecta?
Peter Hull de
1
Não é mais uma questão de evitar C = void? Se Cfosse apenas um tipo indefinido, a deleteinstrução já não falharia?
Kerrek SB de
1
Parece que Mohit Jain tem a resposta.
Peter Hull
1
-1 "Definir um enum de escopo local torna a instrução benigna em tempo de execução." é, uh, sem sentido. Eu sinto Muito.
Saúde e hth. - Alf
1
@SteveJessop obrigado. A parte que estava faltando era que excluir um tipo incompleto é UB.
Peter Hull de
28

Para entender o enum, comece considerando o destruidor sem ele:

~scoped_ptr() {
    delete ptr_;
}

onde ptr_está um C*. Se o tipo Cestiver incompleto neste ponto, isto é, tudo o que o compilador sabe é struct C;, então (1) um destruidor de não fazer nada gerado por padrão é usado para a instância C apontada. É improvável que seja a coisa certa a se fazer para um objeto gerenciado por um ponteiro inteligente.

Se a exclusão por meio de um ponteiro para o tipo incompleto sempre teve comportamento indefinido, o padrão pode apenas exigir que o compilador o diagnostique e falhe. Mas fica bem definido quando o destruidor real é trivial: conhecimento que o programador pode ter, mas o compilador não. Por que a linguagem define e permite isso está além de mim, mas C ++ suporta muitas práticas que hoje não são consideradas melhores práticas.

Um tipo completo tem um tamanho conhecido e, portanto, sizeof(C)irá compilar se e somente se Cfor um tipo completo - com destruidor conhecido. Portanto, ele pode ser usado como um guarda. Uma maneira seria simplesmente

(void) sizeof(C);  // Type must be complete

Eu acho que com algum compilador e opções, o compilador o otimiza antes que ele perceba que não deve ser compilado e que enumé uma maneira de evitar esse comportamento não-conforme do compilador:

enum { type_must_be_complete = sizeof(C) };

Uma explicação alternativa para a escolha, em enumvez de apenas uma expressão descartada, é simplesmente preferência pessoal.

Ou, como sugere James T. Hugget em um comentário a esta resposta, “O enum pode ser uma forma de criar uma mensagem de erro pseudo-portátil em tempo de compilação”.


(1) O destruidor de não fazer nada gerado por padrão para um tipo incompleto era um problema com o antigo std::auto_ptr. Foi tão insidioso que chegou a um item do GOTW sobre o idioma PIMPL , escrito pelo presidente do comitê internacional de padronização C ++ Herb Sutter. Claro, hoje em dia isso std::auto_ptrestá obsoleto, em vez disso, usaremos algum outro mecanismo.

Saúde e hth. - Alf
fonte
4
O enum pode ser uma forma de criar uma mensagem de erro pseudo-portátil em tempo de compilação.
Brice M. Dempsey
1
Acho que essa resposta explica muito bem a motivação do código, mas gostaria de acrescentar que (1) alguns compiladores sizeof(T)avaliam como 0 para tipos incompletos em vez de falhar na compilação. Este é um comportamento não conforme, entretanto. (2) Desde C ++ 11, o uso static_assert((sizeof(T) > 0), "T must be a complete type");seria uma solução superior (e idiomática).
5gon12eder
@ 5gon12eder; Forneça um exemplo de um compilador C ++ que tem “ sizeof(T)avaliação para 0 para tipos incompletos”.
Saúde e hth. - Alf
1
Nunca usei um compilador assim e não ficaria surpreso se soubesse que eles já morreram. E mesmo que não tenham, não se importar com uma implementação não conforme é uma decisão válida. No entanto, ambos libstdc ++ e libc ++ usam static_assert(sizeof(T) > 0, "…");em suas respectivas implementações de std::unique_ptrpara garantir que o tipo esteja completo ...
5gon12eder
1
… Então eu acho que é seguro dizer que isso é idiomático. De qualquer forma, como avaliar sizeof(T)em um contexto booleano é exatamente equivalente a testar sizeof(T) > 0, isso realmente não importa, exceto talvez por razões estéticas.
5gon12eder
3

Talvez um truque para ter certeza Cesteja definido.

Jerome
fonte