Оптимальное возвращение скопированного значения

Я был довольно удивлен, увидев, что Джонатан Вакели, оптимизированный в libstdc++, оптимизирован: https://gcc.gnu.org/ml/libstdc++/2018-05/txtc2I3IxLCfn.txt и попытался расследовать это.

Учитывая пару реализаций concat-функции для любого нетривиального класса, удивительно, что только первый является оптимальным (один экземпляр c-tor и один operator+ = вызов). Другие требуют дополнительного копирования/перемещения вызова c-tor - вы можете проверить его на godbolt.org.

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

Указывает ли спецификация языка такое поведение (если да, почему?) Или это проблема QoI?

Это согласованное поведение между GCC и Clang для всех версий и языковых версий.

void extern_copy();
void extern_move();
void extern_plus_assign();

struct myclass
{
    myclass(const myclass&)
    {
        extern_copy();
    }
    myclass(myclass&&)
    {
        extern_move();
    }
    myclass& operator+=(const myclass&)
    {
        extern_plus_assign();
        return *this;
    }
};

myclass concat(const myclass& lhs, const myclass& rhs)
{
    myclass copy(lhs);
    copy += rhs;
    return copy;
}


myclass concat2(const myclass& lhs, const myclass& rhs)
{
    myclass copy(lhs);
    return copy += rhs;
}

myclass concat3(const myclass& lhs, const myclass& rhs)
{
    return myclass(lhs) += rhs;
}

static myclass concat4impl(myclass lhs, const myclass& rhs)
{
    return lhs += rhs;
}

myclass concat4(const myclass& lhs, const myclass& rhs)
{
    return concat4impl(lhs, rhs);
}


static myclass concat5impl(myclass lhs, const myclass& rhs)
{
    lhs += rhs;
    return lhs;
}

myclass concat5(const myclass& lhs, const myclass& rhs)
{
    return concat5impl(lhs, rhs);
}

Обновление: код изменен, чтобы исключить проблему с последующей реализацией operator+ =:

myclass& myclass::operator+=(myclass const& v) {
  static myclass weird;
  return weird;
}

Ответы

Ответ 1

Копирование elision (RVO, NRVO и специальный случай инициализации) является очень специальной оптимизацией: это единственная оптимизация в C++, которая не распространяется на правило AS-IF. При каждой другой оптимизации компилятор просто модифицирует программу таким образом, чтобы не изменять наблюдаемое поведение, а только производительность. Но RVO может изменять наблюдаемое поведение, потому что побочные эффекты в конструкторах со списком исключенных копий и деструкторах (например, вывод) теряются. Вот почему в стандарте для специального компилятора есть специальная лицензия для выполнения этой оптимизации при очень специфических обстоятельствах.

В частности, RVO применяется только в том случае, если возвращаемое значение является значением prvalue, а NRVO применяется только в том случае, если возвращаемое выражение тривиально относится к локальной переменной.

Возвращаемое выражение в выражении return copy += rhs; нет. Это не тривиально относится к локальной переменной (это выражение составного присваивания, а не id-expression), а возвращаемое значение не является значением prvalue (ваш += перегрузка возвращает ссылку lvalue, делая значение lvalue; это будет разный, если оператор возвращается по значению, но это будет сильно унииоматично в противном случае).

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

Ответ 2

Учти это:

myclass& myclass::operator+=(myclass const& v) {
  static myclass weird;
  return weird;
}

Это совершенно законно!

Ваши рассуждения основаны на том, что мы считаем само собой разумеющимся: что операторы + = всегда будут возвращать *this.

Компилятору не разрешено делать это предположение.

В concat() это не имеет значения, поскольку мы вынуждаем возвращаемое значение concat() быть экземпляром myclass на котором работает + =. Во всех остальных случаях компилятор должен принять худшее, если он не может гарантировать, что + = возвращает *this.