Эффективное использование семантики перемещения вместе с (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?
Спасибо!
3 ответа
Мне нравится измерять, поэтому я настроил это 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;}
};
И я предположил, что некоторые решения могут давать разные ответы для xvalues и 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= C++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 блокируется.
Есть лучший способ сделать это?
Мои любимые р3 и р5. Я предпочитаю p5, а не p4, это просто стилистика. Я уклоняюсь от сдачи move
на return
заявление, когда я знаю, что оно будет применено автоматически из-за страха случайного блокирования RVO. Однако в p5 RVO в любом случае не является опцией, хотя оператор return действительно получает неявный ход. Таким образом, p5 и p4 действительно эквивалентны. Выбери свой стиль.
Если бы мы передали временные параметры, 2-й и 3-й варианты вызвали бы конструктор перемещения при возврате. Можно ли устранить это с помощью (N)RVO?
Столбец "prvalue" против столбца "xvalue" решает этот вопрос. Некоторые решения добавляют дополнительную конструкцию move для xvalues, а некоторые - нет.
Ни одна из показанных вами функций не будет иметь каких-либо существенных оптимизаций возвращаемых значений для своих возвращаемых значений.
makeChanges
возвращает Object&
, Следовательно, оно должно быть скопировано в значение, поскольку вы его возвращаете. Таким образом, первые два всегда делают копию возвращаемого значения. С точки зрения количества копий, первая делает две копии (одна для параметра, одна для возвращаемого значения). Второй делает две копии (одна явно в функции, одна для возвращаемого значения.
Третий не должен даже компилироваться, так как вы не можете неявно преобразовать ссылку на l-значение в ссылку на r-значение.
Так что на самом деле, не делай этого. Если вы хотите передать объект и изменить его на месте, просто сделайте это:
Object &process1(Object &arg) { return arg.makeChanges(); }
Это изменяет предоставленный объект. Нет копирования или что-нибудь. Конечно, можно задаться вопросом, почему process1
не функция-член или что-то, но это не имеет значения.
Самый быстрый способ сделать это - если аргумент равен lvalue, затем скопировать его и вернуть это значение, если rvalue, а затем переместить его. Возврат всегда можно переместить или применить RVO/NRVO. Это легко сделать.
Object process1(Object arg) {
return std::move(arg.makeChanges());
}
Это очень похоже на канонические формы C++11 многих видов перегрузок операторов.