Atualmente, estou lendo o código-fonte de Protocol Buffer
e encontrei um enum
có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?
ptr_
si mesmo nosizeof
como emsizeof(*ptr_)
vez desizeof(C)
.Respostas:
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
sizeof
tipo 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 :.
g++
até emite o seguinte aviso:fonte
sizeof(C)
irá falhar em tempo de compilação seC
não for um tipo completo. Definir um escopo localenum
para 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.fonte
delete
-lo? E em caso afirmativo, por que o compilador não o detecta?C = void
? SeC
fosse apenas um tipo indefinido, adelete
instrução já não falharia?Para entender o
enum
, comece considerando o destruidor sem ele:~scoped_ptr() { delete ptr_; }
onde
ptr_
está umC*
. Se o tipoC
estiver 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 seC
for 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
enum
vez 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 issostd::auto_ptr
está obsoleto, em vez disso, usaremos algum outro mecanismo.fonte
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 usostatic_assert((sizeof(T) > 0), "T must be a complete type");
seria uma solução superior (e idiomática).sizeof(T)
avaliação para 0 para tipos incompletos”.static_assert(sizeof(T) > 0, "…");
em suas respectivas implementações destd::unique_ptr
para garantir que o tipo esteja completo ...sizeof(T)
em um contexto booleano é exatamente equivalente a testarsizeof(T) > 0
, isso realmente não importa, exceto talvez por razões estéticas.Talvez um truque para ter certeza
C
esteja definido.fonte