Какая свобода компилятора для устранения деструктора?

Хорошо известно, что при определенных условиях компилятор может вызвать вызовы конструктора копирования. Однако в Стандарте ясно сказано, что компилятор имеет право изменять поведение во время выполнения (вызов или не конструктор копирования), но перевод выполняется так, как будто вызывается конструктор копирования. В частности, компилятор проверяет, есть ли допустимый конструктор копирования для вызова.

Я столкнулся с ситуацией, когда вызов деструктора может быть устранен, но компиляторы отличаются тем, должен ли существовать действительный деструктор или нет.

Вот полный пример, показывающий, как эта проблема может возникнуть и как компиляторы отличаются поведением.

template <typename T>
struct A {
  ~A() { (void) sizeof(T); }
};

struct B;    // defined elsewhere.

struct C {
  A<B> x, y;
  ~C();      // defined in a TU where B is complete.
};

int main() {
  C c;
}

При компиляции main() компилятор генерирует конструктор по умолчанию C. Этот конструктор по умолчанию инициализирует сначала x, а затем y. Если во время построения y возникает исключение, то x необходимо уничтожить. Сгенерированный код выглядит следующим образом:

new ((void*) &this->x) A<B>;   // default initializes this->x.
try {
  new ((void*) &this->y) A<B>; // default initializes this->y.
}
catch (...) {
  (this->x).~A<B>();           // destroys this->x.
  throw;
}

Зная, что конструктор по умолчанию A<B> тривиален (и не бросает), в соответствии с правилом as-if компилятор может упростить код:

new ((void*) &this->x) A<B>;   // default initializes this->x.
new ((void*) &this->y) A<B>;   // default initializes this->y.

Поэтому нет необходимости вызывать ~A<B>(). (На самом деле, компилятор может даже удалить две инициализации выше, поскольку конструктор A<B> тривиален, но это не важно для этого обсуждения.)

Вопрос: Несмотря на то, что вызов деструктора может быть отменен, должен ли компилятор проверить, доступен ли действительный деструктор? Я не смог найти что-либо в Стандарте, который прояснил бы этот вопрос. Может ли кто-либо предоставлять соответствующие кавычки?

Если компилятор решает не переводить ~A<B>() (например, gcc и Visual Studio do), компиляция завершается успешно.

Однако, если компилятор решает перевести ~A<B>() в любом случае (например, clang и icc do), тогда возникает ошибка, потому что здесь B является неполным типом, и его размер не может быть выполнен.

Ответы

Ответ 1

Я не думаю, что это указано стандартом. Если ~A<B> создается, то он плохо сформирован и требуется диагностика. И, как вы говорите, если построение y бросает, то x должен быть уничтожен.

Однако построение y никогда не может быть брошено, поэтому, возможно, никогда не будет требования к определению деструктора (15.2/2, 14.7.1/3).