В чем разница между тривиальным ctor (или dtor) и пользователем, заданным пустым ctor (или dtor)

Стандарт С++ определяет некоторые очень специфические поведения, когда класс имеет тривиальный конструктор и/или тривиальный деструктор.

В качестве примера, согласно § 3.8/1 стандарта:

Время жизни объекта типа T заканчивается, когда:

- если T - это тип класса с нетривиальным деструктором (12.4), начинается вызов деструктора или

- хранилище, которое занимает объект, повторно используется или освобождается.

Итак,

  • Если объект не является тривиально разрушаемым, любая попытка доступа к элементам объекта после вызова деструктора - это UB.
  • Если объект тривиально разрушаем, попытка доступа к элементам объекта после вызова деструктора является безопасным, а не UB.

Хотя этот пример может быть не лучшим, он показывает, что разница в поведении, возможно, имеет решающее значение (UB/non-UB), является ли объект тривиально разрушаемым или нет.

§12.4/3 Стандарта утверждает, что (для суммирования) деструктор класса T тривиален, если он неявно определен, а не виртуальный, и если все базовые классы и члены класс T тривиально разрушаемы.

В моем (скромном) опыте я никогда не видел разницы с точки зрения кода, сгенерированного компилятором, между:

  • класс с тривиальным значением по умолчанию ctor и/или тривиальным dtor и
  • класс с определяемым пользователем пустым ctor и/или не виртуальным пользователем пустым dtor (если класс, его базовые классы и классы участников также имеют не виртуальный dtor пользователь, определенный пустым или тривиальным)

Итак, мои вопросы:

  • Каким образом пользователь может определить пустой ctor/dtor, может или не может считаться тривиальным ctor/dtor относительно генерации кода компилятора, оптимизации, компромиссов,...
  • Тот же вопрос с пользовательским непустым ctor/dtor; какие правила должны следовать коду, реализованному в ctor/dtor, чтобы рассматривать их как тривиальные.

Мой вопрос не связан со стандартом (пожалуйста, не отвечайте стандартным состояниям, что является тривиальным ctor/dtor, поэтому пользовательский ctor/dtor не является), а в том, как компиляторы имеют дело с определенными пользователем ctor/dtor и в каким образом поведение скомпилированного кода может измениться (или нет) по сравнению с тривиальным ctor/dtor.

Ответ 1

Каким образом пользователь может определить пустой ctor/dtor, может или не может считаться тривиальным ctor/dtor относительно генерации кода компилятора, оптимизации, компромиссов,...

Если конструктор/деструктор не встроены, компилятор может (в зависимости от оптимизации времени ссылки) вызывать вызов для них, даже если они не являются операциями.

Например, следующий код:

struct Struct {
  Struct();
  ~Struct();
};

int main() {
  Struct s;
}

Скомпилирован (с включенными оптимизациями):

main:
        subq    $24, %rsp
        leaq    15(%rsp), %rdi
        call    Struct::Struct()
        leaq    15(%rsp), %rdi
        call    Struct::~Struct()
        xorl    %eax, %eax
        addq    $24, %rsp
        ret

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

Если, однако, вы ввели определения:

struct Struct {
  Struct() {}
  ~Struct() {}
};

Struct foo() {
  return Struct{};
}

Тогда компилятор может (и будет, если он не полностью сосать) обрабатывать их так же, как тривиальные конструкторы/деструкторы:

foo():
        movq    %rdi, %rax
        ret

В этом примере любые вызовы конструктора/деструктора полностью оптимизированы, а сгенерированный код такой же, как если бы определение Struct было простым struct Struct {};.

Тот же вопрос с заданным пользователем непустым ctor/dtor; какие правила должны следовать коду, реализованному в ctor/dtor, чтобы рассматривать их как тривиальные.

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

Однако встроенные непустые конструкторы/деструкторы могут по-прежнему быть "тривиальными", если оптимизатор может полностью их оптимизировать (например, если они содержат только for (int x = 0; x < 1000; ++x);, то это бесполезный код, который можно оптимизировать ) до такой степени, что они эффективно пустые.

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

Ответ 2

Вы знаете стандарты лучше, чем я, но, основываясь на информации, которую вы предоставили, стандарт определяет тривиальный деструктор, но он не определяет пустой деструктор, что сделало бы этот вопрос неправильным. Тривиальный деструктор - это особый случай, который компиляторы могут оптимизировать, и хотя пустой конструктор имеет для нас смысл, это не то, что нужно учитывать разработчикам компилятора.

Просмотр нескольких ссылок SO:

Чтобы ответить на второй вопрос, как только ваш ctor не пуст, это не тривиально. Ближе всего вы добираетесь до тривиального - это пустой ctor/dtor, и ваше тщательное прочтение стандарта уже говорит вам, что это не определено как тривиальное.

TL; DR: Стандарт определяет тривиальный dtor, но не пустой. Смарт-компиляторы могут заметить, что он определен пользователем пустым и рассматривает его как тривиальный, но стандарт не требует такого рассмотрения.