Явно несовместимая копия ctor генерирует лучший код, чем эквивалент, эквивалентный ручному

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

Как clang, так и g++ обрабатывают эту ситуацию аналогичным образом, поэтому мне стало интересно, существует ли для этого основное требование языка, и если да, то что он делает? Ищите цитаты в стандарте, если это возможно. :)

Чтобы показать это в действии, я написал функцию average() двумя способами, работая на raw ints, а также на Holders. Я ожидал, что два будут генерировать один и тот же код. Вот результат:

Явный конструктор копии по умолчанию:

average(Holder, Holder):
  add esi, edi
  mov eax, esi
  shr eax, 31
  add eax, esi
  sar eax
  ret
average(int, int):
  add esi, edi
  mov eax, esi
  shr eax, 31
  add eax, esi
  sar eax
  ret

Это то же самое! Удивительно, правда? Возникает вопрос, когда я забываю "по умолчанию" реализацию и просто вручную пишу версию. До сих пор у меня создалось впечатление, что у этого должен быть тот же результирующий код, что и код по умолчанию, но это не так.

ручной конструктор копирования

average(Holder, Holder):
  mov edx, DWORD PTR [rdx]
  mov ecx, DWORD PTR [rsi]
  mov rax, rdi
  add ecx, edx
  mov edx, ecx
  shr edx, 31
  add edx, ecx
  sar edx
  mov DWORD PTR [rdi], edx
  ret
average(int, int):
  add esi, edi
  mov eax, esi
  shr eax, 31
  add eax, esi
  sar eax
  ret

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

Вот код

#define EXPLICITLY_DEFAULTED_COPY_CTOR true

class Holder {
public:

#if EXPLICITLY_DEFAULTED_COPY_CTOR
    Holder(Holder const & other) = default;
#else
    Holder(Holder const & other) noexcept : value{other.value} { }
#endif 
    constexpr explicit Holder(int value) noexcept : value{value} {}

    Holder& operator+=(Holder rhs) { value += rhs.value; return *this; } 
    Holder& operator/=(Holder rhs) { value /= rhs.value; return *this; } 
    friend Holder operator+(Holder lhs, Holder rhs) { return lhs += rhs; }
    friend Holder operator/(Holder lhs, Holder rhs) { return lhs /= rhs; }    

private:
    int value;
};

Holder average(Holder lhs, Holder rhs) {
    return (lhs + rhs) / Holder{2};
}

int average(int lhs, int rhs) {
    return (lhs + rhs) / int{2};
}

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

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

Смотрите его в прямом эфире на Godbolt: https://godbolt.org/g/YA5Zsq

Ответы

Ответ 1

Это, по-видимому, проблема ABI. В разделе 3.1.1/1 раздела ABI Itanium C++ говорится:

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

а также

Тип считается нетривиальным для целей вызовов, если:

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

Стандарт C++ ссылается на это в [class.temporary]/3:

Когда объект класса X передается или возвращается из функции, если каждый конструктор копирования, перемещение конструктора и деструктор X является либо тривиальным, либо удаленным, а X имеет хотя бы один не удаленный экземпляр или механизм перемещения, реализации разрешено создавать временный объект для хранения параметра функции или объекта результата. Временный объект создается из аргумента функции или возвращаемого значения соответственно, а параметр функции или возвращаемый объект инициализируется так, как если бы с помощью неиспользуемого тривиального конструктора для копирования временного (даже если этот конструктор недоступен или не будет выбран с помощью разрешения перегрузки, чтобы выполнить копию или перемещение объекта). [Примечание. Эта широта предоставляется, чтобы объекты типа класса были переданы или возвращены из функций в регистрах. - конечная нота]


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

Я заметил, что 32-битный g++ делает то же самое. Я не проверял 32-битный ABI; не уверен, имеет ли он аналогичное требование, или g++ просто использовал один и тот же код в обоих случаях.