Почему троичный оператор предотвращает оптимизацию возвращаемого значения?
Почему троичный оператор предотвращает оптимизацию возвращаемого значения (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 просто не реализовала ее. И потому, что в теории они могли бы, возможно, в будущем выпуске они будут.