Почему следует полагаться на оптимизацию именованных возвращаемых значений?

Я читал о NRVO и пытался понять, когда следует полагаться на него, а когда нет. Теперь у меня вопрос: зачем вообще полагаться на NRVO? Всегда можно явно передать возвращаемый параметр по ссылке, поэтому есть ли причина полагаться на NRVO?

2

Решение

Работать с возвращаемыми значениями проще, чем с методами, которые возвращаются путем записи в ссылочный параметр. Рассмотрим следующие 2 метода

C GetByRet() { ... }
void GetByParam(C& returnValue) { ... }

Первая проблема состоит в том, что это делает невозможным цепочку вызовов методов

Method(GetByRet());
// vs.
C temp;
GetByParam(temp);
Method(temp);

Это также делает такие функции, как auto невозможно использовать. Не такая большая проблема для такого типа, как C но более важно для таких типов, как std::map<std::string, std::list<std::string>*>

auto ret = GetByRet();
// vs.
auto value; // Error!
GetByParam(value);

Также, как указал GMacNickG, что если тип C есть частный конструктор, который не может использовать обычный код? Может быть, конструктор private или просто нет конструктора по умолчанию. Снова GetByRet работает как чемпион и GetByParam терпит неудачу

C ret = GetByRet();  // Score!
// vs.
C temp; // Error! Can't access the constructor
GetByParam(temp);
5

Другие решения

Это не ответ, но это также ответ в некотором смысле …

При наличии функции, которая принимает аргумент по указателю, существует тривиальное преобразование, которое даст функцию, которая возвращает значение и тривиально оптимизируется компилятором.

void f(T *ptr) {
// uses ptr->...
}
  1. Добавьте ссылку на объект в функцию и замените все виды использования ptr ссылкой

    void f(T *ptr) { T & obj = *ptr;
    /* uses obj. instead of ptr-> */
    }

  2. Теперь удалите аргумент, добавьте возвращаемый тип, замените T& obj с T obj и измените все возвращаемые значения, чтобы получить ‘obj’

    T f() {
    T obj; // No longer a ref!
    /* code does not change */
    return obj;
    }

  3. На этом этапе у вас есть функция, которая возвращает значение, для которого NRVO тривиально, поскольку все операторы возврата ссылаются на один и тот же объект.

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

Точно такая же стоимость?

Это выходит за рамки языка, но когда компилятор генерирует код, он делает это, следуя ABI (двоичному интерфейсу приложения), который позволяет взаимодействовать при сборке кода различными прогонами компилятора (или даже разными компиляторами на одной платформе). Все используемые в настоящее время ABI имеют общую черту для функции возврата по значению: Для большой (не помещается в регистры) возвращаемых типов, память для возвращаемого объекта выделяется вызывающей стороной, и функция получает дополнительный указатель с расположением этой памяти. Вот когда компилятор видит

T f();

Соглашение о вызовах преобразует это в:

void mangled_name_for_f( T* __result )

Итак, если вы сравните альтернативы: T t; f(&t); а также T t = f(); в обоих случаях сгенерированный код выделяет пространство в кадре вызывающей стороны, [1] вызывает функцию, передающую указатель. В конце функции компилятор вернет [2]. Где [#] — это место, где конструктор объекта фактически вызывается в каждой из альтернатив. Стоимость обеих альтернатив одинакова, с той разницей, что в [1] объект должен быть построен по умолчанию, в то время как в [2] вы уже знаете конечные значения объекта и сможете сделать что-то более эффективное.

Что касается производительности, это все, что есть?

На самом деле, нет. Если позже вам нужно передать этот объект в функцию, которая принимает аргумент по значению, скажите void g(T value)в случае передачи по указателю в стеке вызывающего объекта есть именованный объект, поэтому объект должен быть скопирован (или перемещен) в место, где соглашение о вызовах требует наличия аргумента value. В случае возврата по значению компилятор, зная, что он вызовет g(f()) знает, что единственное использование возвращенного объекта из f() является аргументом g()так что он может просто передать указатель на соответствующее место при вызове f(), что означает, что не будет никаких копий. На данный момент, руководство подход начинает действительно отставать от подхода компилятора даже если реализация f использует тупое преобразование выше!

T obj;    // default initialize
f(&obj);  // assign (or modify in place)
g(obj);   // copy

g(f());   // single object is returned and passed to g(), no copies
3

Фактически НЕ возможно (или желательно) всегда возвращать значение по ссылке (подумайте о operator+ в качестве основного контрпример).

Чтобы ответить на ваш вопрос: вы обычно не полагаться или ожидайте, что NRVO всегда будет происходить, но вы делать ожидайте, что компилятор сделает разумную работу по оптимизации. Только если / когда профилирование указывает, что копирование возвращаемого значения стоит дорого, вам нужно беспокоиться о том, чтобы помочь компилятору с подсказками или альтернативным интерфейсом.

РЕДАКТИРОВАТЬ для some function could be optimized just by using return parameter:

Во-первых, помните, что если функция вызывается не часто или у компилятора достаточно умных умений, вы не можете гарантировать, что параметр return-by-out является оптимизацией. Во-вторых, помните, что у вас будут будущие сопровождающие кода, и что написание понятного кода, пригодного для работы с кодами, — одна из самых больших подсказок, которую вы можете предоставить (не важно, насколько быстро работает поврежденный код). В-третьих, найдите минутку и прочитайте http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/ и посмотрим, может ли это передумать.

1

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

Кроме того, есть много предопределенных операторов, которые возвращают свои результаты по значению (например, арифметические операторы, такие как operator+, operator-, так далее…). Поскольку вы хотите сохранить семантику (и сигнатуру) по умолчанию для таких операторов, вы вынуждены полагаться на NRVO для оптимизации временного объекта, возвращаемого по значению.

Наконец, возврат по значению позволяет упростить цепочку во многих случаях, чем передача параметров, изменяемых неконстантной ссылкой (или указателем).

0
По вопросам рекламы [email protected]