Эффективное использование семантики перемещения вместе с (N) RVO

Скажем, я хочу реализовать функцию, которая должна обрабатывать объект и возвращать новый (возможно, измененный) объект. Я хотел бы сделать это как можно эффективнее в C + 11. Окружающая среда выглядит следующим образом:

class Object {
    /* Implementation of Object */
    Object & makeChanges();
};

Альтернативы, которые приходят мне на ум:

// First alternative:
Object process1(Object arg) { return arg.makeChanges(); }
// Second alternative:
Object process2(Object const & arg) { return Object(arg).makeChanges(); }
Object process2(Object && arg) { return std::move(arg.makeChanges()); }
// Third alternative:
Object process3(Object const & arg) { 
    Object retObj = arg; retObj.makeChanges(); return retObj; 
}
Object process3(Object && arg) { std::move(return arg.makeChanges()); }

Примечание. Я хотел бы использовать функцию обертки, такую ​​как process(), потому что она будет выполнять некоторую другую работу, и я хотел бы иметь как можно больше повторного использования кода.

Обновление:

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

Попытка их с помощью clang [т.е. Object obj2 = process(obj);] приводит к следующему:

Первая опция делает два вызова конструктора копирования; один для передачи аргумента и один для возврата. Вместо этого можно сказать return std::move(..) и иметь один вызов конструктора копирования и один вызов конструктора перемещения. Я понимаю, что RVO не может избавиться от одного из этих вызовов, потому что мы имеем дело с параметром функции.

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

В третьем варианте у нас есть только один вызов конструктора копирования, и это явный. (N) RVO исключает вызов конструктора копирования, который мы будем делать для возврата.

Мои вопросы таковы:

  • (ответил) Почему RVO пинает последний параметр, а не второй?
  • Есть ли лучший способ сделать это?
  • Если бы мы проходили во временном, 2-м и 3-м вариантах, вы вызывали бы конструктор перемещения при возврате. Возможно ли исключить использование (N) RVO?

Спасибо!

Ответ 1

Мне нравится измерять, поэтому я установил этот Object:

#include <iostream>

struct Object
{
    Object() {}
    Object(const Object&) {std::cout << "Object(const Object&)\n";}
    Object(Object&&) {std::cout << "Object(Object&&)\n";}

    Object& makeChanges() {return *this;}
};

И я предположил, что некоторые решения могут давать разные ответы на значения x и prvalues ​​(оба из которых являются rvalues). И поэтому я решил проверить их обоих (в дополнение к lvalues):

Object source() {return Object();}

int main()
{
    std::cout << "process lvalue:\n\n";
    Object x;
    Object t = process(x);
    std::cout << "\nprocess xvalue:\n\n";
    Object u = process(std::move(x));
    std::cout << "\nprocess prvalue:\n\n";
    Object v = process(source());
}

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

#if PROCESS == 1

Object
process(Object arg)
{
    return arg.makeChanges();
}

#elif PROCESS == 2

Object
process(const Object& arg)
{
    return Object(arg).makeChanges();
}

Object
process(Object&& arg)
{
    return std::move(arg.makeChanges());
}

#elif PROCESS == 3

Object
process(const Object& arg)
{
    Object retObj = arg;
    retObj.makeChanges();
    return retObj; 
}

Object
process(Object&& arg)
{
    return std::move(arg.makeChanges());
}

#elif PROCESS == 4

Object
process(Object arg)
{
    return std::move(arg.makeChanges());
}

#elif PROCESS == 5

Object
process(Object arg)
{
    arg.makeChanges();
    return arg;
}

#endif

В приведенной ниже таблице представлены мои результаты (с использованием clang -std = С++ 11). Первое число - это число копий, а второе число - количество перемещаемых конструкций:

+----+--------+--------+---------+
|    | lvalue | xvalue | prvalue |    legend: copies/moves
+----+--------+--------+---------+
| p1 |  2/0   |  1/1   |   1/0   |
+----+--------+--------+---------+
| p2 |  2/0   |  0/1   |   0/1   |
+----+--------+--------+---------+
| p3 |  1/0   |  0/1   |   0/1   |
+----+--------+--------+---------+
| p4 |  1/1   |  0/2   |   0/1   |
+----+--------+--------+---------+
| p5 |  1/1   |  0/2   |   0/1   |
+----+--------+--------+---------+

process3 выглядит как лучшее решение для меня. Однако для этого требуется две перегрузки. Один для обработки lvalues ​​и один для обработки rvalues. Если по какой-то причине это является проблематичным, решения 4 и 5 выполняют работу только с одной перегрузкой за счет 1 дополнительной конструкции перемещения для glvalues ​​(lvalues ​​и xvalues). Речь идет о том, хотите ли вы заплатить дополнительную конструкцию движения, чтобы сохранить перегрузку (и нет ни одного правильного ответа).

(ответил) Почему RVO пинает последний параметр, а не второй?

Для запуска RVO оператор return должен выглядеть так:

return arg;

Если вы затруднились с этим:

return std::move(arg);

или

return arg.makeChanges();

тогда RVO блокируется.

Есть ли лучший способ сделать это?

Мои фавориты - p3 и p5. Мое предпочтение p5 над p4 просто стилистично. Я уклоняюсь от размещения move в заявлении return, когда я знаю, что он будет применяться автоматически, опасаясь случайного блокирования RVO. Однако в p5 RVO в любом случае не является вариантом, хотя оператор return получает неявный ход. Таким образом, p5 и p4 действительно эквивалентны. Выберите свой стиль.

Если бы мы прошли временные, 2-й и 3-й варианты, это вызовет переход конструктор при возврате. Можно ли исключить это использование (N), РВО?

Столбец "prvalue" против столбца "xvalue" обращается к этому вопросу. Некоторые решения добавляют дополнительную конструкцию перемещения для значений x, а некоторые - нет.

Ответ 2

Ни одна из функций, которые вы покажете, не будет иметь никаких значительных оптимизаций возвращаемых значений по их возвращаемым значениям.

makeChanges возвращает Object&. Поэтому он должен быть скопирован в значение, так как вы его возвращаете. Таким образом, первые два будут всегда делать копию возвращаемого значения. Что касается количества копий, первая делает две копии (одна для параметра, другая для возвращаемого значения). Вторая делает две копии (одна явно в функции, одна для возвращаемого значения.

Третий не должен компилироваться, так как вы не можете неявно преобразовать ссылку l-value в ссылку r-value.

Итак, не делайте этого. Если вы хотите передать объект и изменить его на месте, просто выполните это:

Object &process1(Object &arg) { return arg.makeChanges(); }

Это изменяет предоставленный объект. Никакого копирования или чего-либо еще. Конечно, можно задаться вопросом, почему process1 не является функцией-членом или чем-то, но это не имеет значения.

Ответ 3

Самый быстрый способ сделать это - если аргумент равен lvalue, затем скопируйте его и верните значение copy- if rvalue, а затем переместите его. Возврат всегда можно переместить или применить RVO/NRVO. Это легко осуществить.

Object process1(Object arg) {
    return std::move(arg.makeChanges());
}

Это очень похоже на канонические формы С++ 11 многих видов перегрузок операторов.