Почему троичный оператор предотвращает оптимизацию возвращаемого значения?

Почему троичный оператор предотвращает оптимизацию возвращаемого значения (RVO) в MSVC? Рассмотрим следующую полную примерную программу:

#include <iostream>

struct Example
{
    Example(int) {}
    Example(Example const &) { std::cout << "copy\n"; }
};

Example FunctionUsingIf(int i)
{
    if (i == 1)
        return Example(1);
    else
        return Example(2);
}

Example FunctionUsingTernaryOperator(int i)
{
    return (i == 1) ? Example(1) : Example(2);
}

int main()
{
    std::cout << "using if:\n";
    Example obj1 = FunctionUsingIf(0);
    std::cout << "using ternary operator:\n";
    Example obj2 = FunctionUsingTernaryOperator(0);
}

Скомпилирован с VC 2013: cl /nologo /EHsc /Za /W4 /O2 stackoverflow.cpp

Вывод:

using if:
using ternary operator:
copy

Таким образом, очевидно, что троянный оператор каким-то образом предотвращает RVO. Зачем? Почему компилятор не был бы достаточно умен, чтобы увидеть, что функция, использующая тернарный оператор, выполняет ту же функцию, что и оператор if, и оптимизируется соответственно?

Ответ 1

Глядя на вывод программы, мне кажется, что, в самом деле, компилятор возвращается в обоих случаях, почему?

Поскольку, если не было активировано elide, правильным выходом будет:

  • построить объект example при возврате функции;
  • скопируйте его во временное;
  • скопировать временный объект, определенный в основной функции.

Итак, я ожидал бы, по крайней мере, 2 "копировать" вывод на моем экране. Действительно, если я исполняю вашу программу, скомпилированную с g++, с -fno-elide-constructor, у меня есть 2 копии сообщений от каждой функции.

Интересно, если я сделаю то же самое с clang, я получил сообщение с 3 "копиями", когда вызывается функция FunctionUsingTernaryOperator(0);, и, я думаю, это связано с тем, как тройка реализуется компилятором. Я предполагаю, что это создает временное решение для тернарного оператора и копирование этого временного выражения в оператор return.

Ответ 2

Этот связанный вопрос содержит ответ.

В стандарте указано, разрешено ли копирование или перемещение в выражении возврата: (12.8.31)

  • в операторе return в функции с типом возвращаемого класса, когда выражение является именем энергонезависимого автоматического объекта (кроме функции или параметра catch-clause) с тем же самым cvunqualified типом, что и тип возврата функции, операцию копирования/перемещения можно опустить, построив автоматический объект непосредственно в возвращаемое значение функции
  • когда объект временного класса, который не был привязан к ссылке (12.2), будет скопирован/перенесен в объект класса с тем же самым cv-неквалифицированным типом, операция копирования/перемещения может быть опущена путем непосредственного конструирования временного объекта в цель пропущенной копии/перемещения

Таким образом, в основном копирование происходит только в следующих случаях:

  • возвращает именованный объект.
  • возврат временного объекта

Если ваше выражение не является именованным объектом или временным, вы возвращаетесь к копированию.

Некоторые интересные поведения:

  • return (name); не препятствует копированию (см. этот вопрос)
  • return true?name:name; должен предотвращать копирование, но gcc 4.6 по крайней мере неправильно на этом (см. этот вопрос)

EDIT:

Я оставил свой первоначальный ответ выше, но Христиан Хакл прав в своем комментарии, он не отвечает на вопрос.

Что касается правил, троянный оператор в примере дает временный объект, поэтому 12.8.31 позволяет исключить копирование/перемещение. Итак, с точки зрения языка С++. Компилятору разрешено исключать копию при возврате из FunctionUsingTernaryOperator.

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