Странное перечисление в деструкторе

В настоящее время я читаю исходный код Protocol Buffer, и я нашел один странный код enum, определенный здесь

  ~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;
    }
  }

Почему здесь указан enum { type_must_be_complete = sizeof(C) };? для чего он используется?

Ответ 1

Этот трюк позволяет избежать UB, гарантируя, что определение C доступно при компиляции этого деструктора. В противном случае компиляция завершится неудачно, поскольку неполный тип sizeof (пересылаемые объявленные типы) не может быть определен, но можно использовать указатели.

В скомпилированном двоичном коде этот код будет оптимизирован и не будет иметь никакого эффекта.

Обратите внимание: Удаление неполного типа может быть undefined поведение из 5.3.5/5:.

если удаляемый объект имеет тип неполный класс в точке удаление, а полный класс имеет нетривиальный деструктор или deallocation, поведение undefined.

g++ даже выдает следующее предупреждение:

предупреждение: возможная проблема, обнаруженная при вызове delete оператор:
предупреждение: 'p' имеет недопустимый тип
предупреждение: вперед объявление 'struct C'

Ответ 2

sizeof(C) завершится с ошибкой во время компиляции, если C не является полным типом. Установка локальной области enum в нее делает утверждение доброкачественным во время выполнения.

Это способ программиста, защищающий себя от себя: поведение последующего delete ptr_ на неполном типе undefined, если оно имеет нетривиальный деструктор.

Ответ 3

Чтобы понять enum, начните с рассмотрения деструктора без него:

~scoped_ptr() {
    delete ptr_;
}

где ptr_ - C*. Если тип C является неполным в этой точке, то есть все, что знает компилятор, это struct C;, тогда (1) созданный по умолчанию деструктор do-nothing используется для указанного экземпляра C. Это вряд ли будет правильным для объекта, управляемого умным указателем.

Если удаление с помощью указателя на неполный тип всегда имело Undefined Behavior, то стандарт мог просто потребовать, чтобы компилятор его диагностировал и завершил сбой. Но он четко определен, когда реальный деструктор тривиален: знание, которое может иметь программист, но компилятор его не имеет. Почему язык определяет и позволяет это превыше меня, но С++ поддерживает многие практики, которые сегодня не рассматриваются как лучшие практики.

Полный тип имеет известный размер, и, следовательно, sizeof(C) будет компилироваться тогда и только тогда, когда C является полным типом - с известным деструктором. Поэтому его можно использовать в качестве охранника. Один из способов - просто

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

Я бы предположил, что с некоторыми компиляторами и параметрами компилятор оптимизирует его, прежде чем он сможет заметить, что он не должен компилироваться и что enum - это способ избежать такого несоответствующего поведения компилятора:

enum { type_must_be_complete = sizeof(C) };

Альтернативное объяснение выбора enum, а не просто отброшенное выражение, является просто личным предпочтением.

Или как Джеймс Т. Хаггет предлагает в комментарии к этому ответу: "Перечисление может быть способом создания псевдопортативного сообщения об ошибке во время компиляции".


(1) Созданный по умолчанию деструктор do-nothing для неполного типа был проблемой со старым std::auto_ptr. Это было настолько коварно, что он пробрался в элемент GOTW о идиоме PIMPL, написанный председателем международного комитета по стандартизации С++ Херб Саттер, Конечно, в настоящее время std::auto_ptr устарел, вместо этого будет использоваться какой-то другой механизм.

Ответ 4

Может быть, трюк, чтобы быть уверенным, что C определен.