Можем ли мы использовать оптимизацию возвращаемого значения, когда это возможно, и отступать назад, а не копировать, семантику, если нет?

Можно ли написать код на С++, где мы по возможности полагаемся на оптимизацию возвращаемого значения (RVO), но не возвращаемся к семантике перемещения, если нет? Например, следующий код не может использовать RVO из-за условного, поэтому он копирует результат:

#include <iostream>

struct Foo {
    Foo() {
        std::cout << "constructor" << std::endl;
    }
    Foo(Foo && x) {
        std::cout << "move" << std::endl;
    }
    Foo(Foo const & x) {
        std::cout << "copy" << std::endl;
    }
    ~Foo() {
        std::cout << "destructor" << std::endl;
    }
};

Foo f(bool b) {
    Foo x;
    Foo y;
    return b ? x : y;  
}

int main() {
   Foo x(f(true));
   std::cout << "fin" << std::endl;
}

Это дает

constructor
constructor
copy
destructor
destructor
fin
destructor

что имеет смысл. Теперь я могу заставить конструктор перемещения вызываться в приведенном выше коде, изменив строку

    return b ? x : y;  

к

    return std::move(b ? x : y);

Это дает выход

constructor
constructor
move
destructor
destructor
fin
destructor

Однако мне не очень нравится называть std:: move напрямую.

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


Изменить 1

Следующий question помог мне разобраться, что происходит. В основном, 12.8.32 стандартных состояний:

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

Хорошо, поэтому, чтобы выяснить, каковы критерии для копии elison, посмотрим на 12.8.31

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

Таким образом, если мы определим код для f как:

Foo f(bool b) {
    Foo x;
    Foo y;
    if(b) return x;
    return y;
}

Затем каждое из наших возвращаемых значений является автоматическим объектом, поэтому в 12.8.31 говорится, что он подходит для копирования elison. Это достигает 12.8.32, в котором говорится, что копия выполняется так, как если бы она была rvalue. Теперь RVO не происходит, потому что мы не знаем априори, какой путь взять, но конструктор перемещения вызван из-за требований в 12.8.32. Технически один конструктор перемещения избегается при копировании в x. В основном, при запуске мы получаем:

constructor
constructor
move
destructor
destructor
fin
destructor

Отключение elide на конструкторах генерирует:

constructor
constructor
move
destructor
destructor
move
destructor
fin
destructor

Теперь, скажем, мы вернемся к

Foo f(bool b) {
    Foo x;
    Foo y;
    return b ? x : y;
}

Мы должны смотреть на семантику условного оператора в 5.16.4

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

Поскольку оба x и y являются lvalues, условный оператор является lvalue, но не является автоматическим объектом. Таким образом, 12.8.32 не срабатывает, и мы возвращаем возвращаемое значение как lvalue, а не rvalue. Для этого требуется вызвать конструктор копирования. Следовательно, получаем

constructor
constructor
copy
destructor
destructor
fin
destructor

Теперь, поскольку условный оператор в этом случае в основном копирует категорию значений, это означает, что код

Foo f(bool b) {
    return b ? Foo() : Foo();
}

вернет rvalue, так как обе ветки условного оператора являются значениями r. Мы видим это следующим образом:

constructor
fin
destructor

Если мы отключим elide на конструкторах, мы увидим движения

constructor
move
destructor
move
destructor
fin
destructor

В принципе, идея состоит в том, что если мы вернем rvalue, мы назовем конструктор перемещения. Если мы вернем lvalue, мы назовем конструктор копирования. Когда мы возвращаем энергонезависимый автоматический объект, тип которого соответствует типу возвращаемого типа, мы возвращаем значение rvalue. Если у нас есть достойный компилятор, эти копии и ходы могут быть отменены с помощью RVO. Однако, по крайней мере, мы знаем, какой конструктор вызывается в случае, если RVO не может быть применен.

Ответ 1

Когда выражение в операторе return является энергонезависимым автоматическим объектом продолжительности, а не параметром функции или catch-clause, с тем же cv-неквалифицированным типом, что и тип возвращаемой функции, результирующая копия/перемещение имеет право на копировать. В стандарте также говорится, что, если единственной причиной отказа в копировании было то, что исходный объект был функциональным параметром, и если компилятор не смог лишить копию, разрешение перегрузки для копии должно быть выполнено так, как если бы выражение было rvalue. Таким образом, он предпочел бы конструктор перемещения.

OTOH, так как вы используете тернарное выражение, ни одно из условий не выполняется, и вы застряли с обычной копией. Изменение кода на

if(b)
  return x;
return y;

вызывает конструктор перемещения.

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

Ответ 2

Да, есть. Не возвращайте результат тернарного оператора; используйте if/else. Когда вы возвращаете локальную переменную напрямую, перемещение семантики используется, когда это возможно. Тем не менее, в вашем случае вы не возвращаете локальный адрес напрямую - вы возвращаете результат выражения.

Если вы измените свою функцию следующим образом:

Foo f(bool b) {
    Foo x;
    Foo y;
    if (b) { return x; }
    return y;
}

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

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

Если вам не нравится этот подход, я предлагаю вам придерживаться std::move. Возможно, вам это не понравится, но вы должны выбрать свой яд - язык такой, каким он есть.