Эффективное использование семантики перемещения вместе с (N) RVO - PullRequest
5 голосов
/ 31 марта 2012

Допустим, я хочу реализовать функцию, которая должна обрабатывать объект и возвращать новый (возможно, измененный) объект.Я хотел бы сделать это максимально эффективно в 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 исключает вызов конструктора копирования, который мы будем делать для возврата.

Мои вопросы следующие:

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

Спасибо!

Ответы [ 3 ]

17 голосов
/ 31 марта 2012

Мне нравится измерять, поэтому я настроил это 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 и значений prvalue (оба из которых являются значениями r).И поэтому я решил проверить их обоих (в дополнение к 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 блокируется.

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

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

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

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

2 голосов
/ 31 марта 2012

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

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

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

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

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

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

0 голосов
/ 31 марта 2012

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

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

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

...