Enum extraño en destructor

83

Actualmente, estoy leyendo el código fuente de Protocol Buffer, y encontré un enumcódigo extraño definido aquí

  ~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 qué enum { type_must_be_complete = sizeof(C) };se define aquí? ¿Para qué se usa esto?

zangw
fuente
2
Si quiero estar tan seguro, prefiero usarlo ptr_en el sizeofcomo en sizeof(*ptr_)lugar de sizeof(C).
Nawaz

Respuestas:

81

Este truco evita UB al garantizar que la definición de C esté disponible cuando se compile este destructor. De lo contrario, la compilación fallaría ya que el sizeoftipo incompleto (tipos declarados hacia adelante) no se puede determinar, pero se pueden usar los punteros.

En binario compilado, este código se optimizaría y no tendría ningún efecto.

Tenga en cuenta que: Eliminar el tipo incompleto puede ser un comportamiento indefinido de 5.3.5 / 5 :.

si el objeto que se elimina tiene un tipo de clase incompleto en el punto de eliminación y la clase completa tiene un destructor no trivial o una función de desasignación, el comportamiento no está definido .

g++ incluso emite la siguiente advertencia:

advertencia: posible problema detectado en la invocación del operador de eliminación:
advertencia: 'p' tiene un tipo incompleto
advertencia: declaración de avance de 'estructura C'

Mohit Jain
fuente
1
"Eliminar un tipo incompleto es un comportamiento indefinido" es incorrecto. Solo es UB si el tipo tiene un destructor no trivial. El problema que aborda este pequeño truco es precisamente que borrar el tipo incompleto no siempre es UB, por lo que el lenguaje lo soporta.
Saludos y hth. - Alf
Gracias @ Cheersandhth.-Alf Mi punto es que puede ser UB, así que en general esta línea de código es UB. Editado.
Mohit Jain
32

sizeof(C)fallará en tiempo de compilación si Cno es un tipo completo. Establecer un ámbito local enumhace que la declaración sea benigna en tiempo de ejecución.

Es una forma en que el programador se protege de sí mismo: el comportamiento de un tipo subsiguiente delete ptr_en un tipo incompleto no está definido si tiene un destructor no trivial.

Betsabé
fuente
1
¿Puede explicar por qué C necesita ser un tipo completo en ese momento? ¿Es necesario tener la definición de tipo completa para invocarlo delete? Y si es así, ¿por qué el compilador no lo detecta de todos modos?
Peter Hull
1
¿No se trata más bien de evitarlo C = void? Si Cfuera simplemente un tipo indefinido, ¿no deletefallaría ya la declaración?
Kerrek SB
1
Parece que Mohit Jain tiene la respuesta.
Peter Hull
1
−1 "Establecer una enumeración de ámbito local hace que la declaración sea benigna en tiempo de ejecución". no tiene sentido. Lo siento.
Saludos y hth. - Alf
1
@SteveJessop gracias. La parte que me faltaba era que eliminar un tipo incompleto es UB.
Peter Hull
28

Para entender el enum, comience considerando el destructor sin él:

~scoped_ptr() {
    delete ptr_;
}

donde ptr_esta a C*. Si el tipo Ces incompleta en este punto, es decir, todo lo que el compilador sabe que es struct C;, a continuación, (1) un defecto generado no hacer nada destructor se utiliza para la instancia C señaló. Es poco probable que eso sea lo correcto para un objeto administrado por un puntero inteligente.

Si la eliminación mediante un puntero a tipo incompleto siempre tuvo un comportamiento indefinido, entonces el estándar podría requerir que el compilador lo diagnostique y falle. Pero está bien definido cuando el destructor real es trivial: conocimiento que el programador puede tener, pero el compilador no. No entiendo por qué el lenguaje define y permite esto, pero C ++ admite muchas prácticas que hoy en día no se consideran mejores prácticas.

Un tipo completo tiene un tamaño conocido y, por lo tanto, sizeof(C)se compilará si y solo si Ces un tipo completo, con un destructor conocido. Por lo que se puede utilizar como guardia. Una forma sería simplemente

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

Me supongo que con un poco compilador y las opciones del compilador optimiza la basura antes de que pudiera notar que no debería compilar, y que el enumes una manera de evitar este tipo de comportamiento no conforme compilador:

enum { type_must_be_complete = sizeof(C) };

Una explicación alternativa para la elección de enumuna expresión en lugar de simplemente descartada, es simplemente una preferencia personal.

O como sugiere James T. Hugget en un comentario a esta respuesta, "La enumeración puede ser una forma de crear un mensaje de error pseudoportátil en tiempo de compilación".


(1) El destructor de hacer nada generado por defecto para un tipo incompleto era un problema con el antiguo std::auto_ptr. Fue tan insidioso que se abrió paso en un artículo de GOTW sobre el idioma PIMPL , escrito por el presidente del comité internacional de estandarización de C ++ Herb Sutter. Por supuesto, hoy en día eso std::auto_ptrestá desaprobado, en su lugar se utilizará algún otro mecanismo.

Saludos y hth. - Alf
fuente
4
La enumeración puede ser una forma de crear un mensaje de error pseudoportátil en tiempo de compilación.
Brice M. Dempsey
1
Creo que esta respuesta explica muy bien la motivación del código, pero me gustaría agregar que (1) algunos compiladores han sizeof(T)evaluado a 0 para tipos incompletos en lugar de fallar en la compilación. Sin embargo, este es un comportamiento no conforme. (2) Desde C ++ 11, el uso static_assert((sizeof(T) > 0), "T must be a complete type");sería una solución superior (e idiomática).
5gon12eder
@ 5gon12eder; Proporcione un ejemplo de un compilador de C ++ que tiene " sizeof(T)evaluar a 0 para tipos incompletos".
Saludos y hth. - Alf
1
Nunca he usado un compilador de este tipo y no me sorprendería saber que ya se han extinguido. E incluso si no lo han hecho, no preocuparse por una implementación no conforme es una decisión válida. Sin embargo, tanto libstdc ++ como libc ++ usan static_assert(sizeof(T) > 0, "…");en sus respectivas implementaciones de std::unique_ptrpara asegurarse de que el tipo esté completo…
5gon12eder
1
… Entonces creo que es seguro decir que esto es idiomático. De todos modos, dado que evaluar sizeof(T)en un contexto booleano es exactamente equivalente a probar sizeof(T) > 0, en realidad no importa, excepto quizás por razones estéticas.
5gon12eder
3

Tal vez Cse defina un truco para estar seguro .

Jerome
fuente