Подтвердить что ты не робот

Передать значение vs pass по ссылке rvalue

Когда я должен объявить свою функцию как:

void foo(Widget w);

в отличие от

void foo(Widget&& w);?

Предположим, что это единственная перегрузка (как, например, я выбираю одну или другую, а не обе и другие перегрузки). Нет шаблонов. Предположим, что функция foo требует владения Widget (например, const Widget& не является частью этого обсуждения). Меня не интересует какой-либо ответ вне сферы этих обстоятельств. См. Добавление в конце сообщения, почему эти ограничения являются частью вопроса.

Основное различие, которое могут возникнуть у моих коллег, состоит в том, что ссылочный параметр rvalue заставляет вас быть явно о копиях. Вызывающий отвечает за создание явной копии, а затем передает ее с помощью std::move, когда вы хотите получить копию. В случае прохождения по значению стоимость копии скрыта:

    //If foo is a pass by value function, calling + making a copy:
    Widget x{};
    foo(x); //Implicit copy
    //Not shown: continues to use x locally

    //If foo is a pass by rvalue reference function, calling + making a copy:
    Widget x{};
    //foo(x); //This would be a compiler error
    auto copy = x; //Explicit copy
    foo(std::move(copy));
    //Not shown: continues to use x locally

Кроме этой разницы. Кроме того, чтобы заставить людей быть явным о копировании и изменении того, сколько синтаксического сахара вы получаете при вызове функции, как еще они отличаются? Что они говорят по-разному о интерфейсе? Являются ли они более или менее эффективными друг другом?

Другие вещи, о которых я и мои коллеги уже подумали:

  • Параметр ссылочного значения rvalue означает, что вы можете переместить аргумент, но он не дает ему мандат. Возможно, после этого аргумент, который вы передали на сайте вызова, будет в исходном состоянии. Также возможно, что функция будет есть/изменять аргумент, даже не называя конструктор перемещения, но предполагая, что, поскольку это была ссылка на rvalue, вызывающий абонент отказался от контроля. Переходите по значению, если вы перейдете в него, вы должны предположить, что произошел переход; нет выбора.
  • Предполагая никаких исключений, вызов конструктора с одним движением исключается с помощью pass by rvalue.
  • У компилятора есть лучшая возможность для элиминации копий/ходов с передачей по значению. Может ли кто-нибудь обосновать это требование? Предпочтительно ссылка на gcc.godbolt.org, показывающая оптимизированный сгенерированный код из gcc/clang, а не строка в стандарте. Моя попытка показать это, вероятно, не смогла успешно изолировать поведение: https://godbolt.org/g/4yomtt

Добавление: Почему я так сильно ограничиваю эту проблему?

  • Никаких перегрузок - если бы были другие перегрузки, это перешло бы в обсуждение прохода по значению vs набора перегрузок, которые включают в себя ссылку на const и rvalue, после чего набор перегрузок, очевидно, более эффективен и выигрывает. Это хорошо известно, и поэтому не интересно.
  • Нет шаблонов - меня не интересует, как перенаправление ссылок вписывается в изображение. Если у вас есть ссылка на пересылку, вы все равно вызываете std:: forward. Цель с ссылкой на пересылку - передать все, как вы их получили. Копии не актуальны, потому что вы просто передаете lvalue. Это хорошо известно, и не интересно.
  • foo требует права собственности на Widget (aka no const Widget&). Мы не говорим о функциях только для чтения. Если функция была доступна только для чтения или ей не нужно было владеть или продлевать время жизни Widget, тогда ответ тривиально становится const Widget&, что опять же хорошо известно и не интересно. Я также ссылаюсь на то, почему мы не хотим говорить о перегрузках.
4b9b3361

Ответ 1

Параметр rvalue указывает на то, что вы явно указываете на копии.

Да, ссылка "pass-by-rvalue-reference" получила точку.

Параметр rvalue reference означает, что вы можете перемещать аргумент, но не наделяете его полномочиями.

Да, пропускная способность получила точку. Но это также дает возможность перепродавать возможность справиться с исключительной гарантией: если foo throws, значение widget не потребляется.

Для типов только для перемещения (как std::unique_ptr) значение по умолчанию кажется нормой (в основном для вашей второй точки, а первая точка не применяется в любом случае).

EDIT: стандартная библиотека противоречит моему предыдущему предложению, один из конструкторов shared_ptr принимает std::unique_ptr<T, D>&&.

Для типов, которые имеют как copy/move (как std::shared_ptr), у нас есть выбор согласованности с предыдущими типами или силой, чтобы быть явным при копировании.

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

Для существующей базы кода я бы сохранил согласованность.

Ответ 2

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

Я думаю, что выбор частично зависит от того, что foo будет делать с параметром.

Функция нуждается в локальной копии

Скажем, Widget - это итератор, и вы хотите реализовать свою собственную функцию std::next. next нужна его собственная копия для продвижения, а затем возврата. В этом случае ваш выбор выглядит примерно так:

Widget next(Widget it, int n = 1){
    std::advance(it, n);
    return it;
}

против

Widget next(Widget&& it, int n = 1){
    std::advance(it, n);
    return std::move(it);
}

Я думаю, что здесь полезная ценность. Из подписи вы видите, что она берет копию. Если вызывающий абонент хочет избежать копирования, он может сделать std::move и гарантировать, что переменная перемещена, но они все равно могут передавать lvalues, если захотят. С помощью ссылки pass-by-rvalue вызывающий абонент не может гарантировать, что переменная была перенесена.

Перемещение назначения в копию

Скажем, у вас есть класс WidgetHolder:

class WidgetHolder {
    Widget widget;
   //...
};

и вам нужно реализовать функцию-член setWidget. Я предполагаю, что у вас уже есть перегрузка, которая принимает ссылку-на-const:

WidgetHolder::setWidget(const Widget& w) {
    widget = w;
}

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

WidgetHolder::setWidget(Widget w) {
    widget = std::move(w);
}

Или перегрузка с помощью:

WidgetHolder::setWidget(Widget&& widget) {
    widget = std::move(w);
}

Это немного сложнее. Заманчиво выбирать pass-by-value, потому что он принимает как rvalues, так и lvalues, поэтому вам не нужны две перегрузки. Однако он безоговорочно берет копию, поэтому вы не можете использовать любую существующую емкость в переменной-члене. Передача по ссылке-на-const и передача по опорным перегрузкам r-значения использует назначение без копирования, которое может быть быстрее

Переместить-построить копию

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

WidgetHolder::WidgetHolder(const Widget& w) : widget(w) {
}

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

WidgetHolder::WidgetHolder(Widget w) : widget(std::move(w)) {
}

Или перегрузка с помощью:

WidgetHolder::WidgetHolder(Widget&& w) : widget(std:move(w)) {
}

В этом случае переменная-член не может иметь никакой существующей емкости, так как это конструктор. Вы скопируете копию. Кроме того, конструкторы часто принимают множество параметров, поэтому может быть довольно больно писать все разные перестановки перегрузок для оптимизации ссылок на r-значение. Поэтому в этом случае рекомендуется использовать pass-by-value, особенно если конструктор принимает много таких параметров.

Передача unique_ptr

С unique_ptr соображения эффективности менее важны, учитывая, что движение настолько дешево, что оно не имеет возможности. Еще важнее выразительность и правильность. Существует хорошее обсуждение того, как пройти unique_ptr здесь.

Ответ 3

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

Сначала у вас есть класс RAII

void fn(RAII &&);

RAII x{underlying_resource};
fn(std::move(x));
// later in the code
RAII y{underlying_resource};

При инициализации y ресурс все равно может удерживаться x, если fn не перемещается из ссылки rvalue. В прохождении по коду значений мы знаем, что x выводится из и fn выпускает x. Вероятно, это случай, когда вы хотите передать значение по значению, и конструктор копирования, скорее всего, будет удален, поэтому вам не придется беспокоиться о случайных копиях.

Во-вторых, если аргумент является большим объектом и функция не выходит из строя, время жизни данных векторов больше, чем в случае прохождения по значению.

vector<B> fn1(vector<A> &&x);
vector<C> fn2(vector<B> &&x);

vector<A> va;  // large vector
vector<B> vb = fn1(std::move(va));
vector<C> vc = fn2(std::move(vb));

В приведенном выше примере, если fn1 и fn2 не перемещаются из x, тогда вы получите все данные во всех переносимых векторах. Если вы переходите по значению, только последние векторные данные будут по-прежнему оставаться в живых (при условии, что конструкторы перемещения векторов очищают вектор источников).

Ответ 4

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

В общем случае, если функция генерирует исключение, мы бы идеально хотели бы иметь сильную гарантию исключения, а это означает, что вызов не имеет никакого эффекта, кроме повышения исключения. Если pass-by-value использует конструктор перемещения, то такой эффект по существу неизбежен. Таким образом, аргумент rvalue-reference может быть выше в некоторых случаях. (Конечно, существуют различные случаи, когда сильная гарантия исключений не достижима в любом случае, а также в различных случаях, когда гарантия отсутствия бросков доступна в любом случае. Поэтому это не имеет значения в 100% случаев. иногда.)

Ответ 5

Выбор значения by-value и by-rvalue-ref без каких-либо других перегрузок не имеет смысла.

С передачей по значению фактический аргумент может быть выражением lvalue.

С pass by rvalue-ref фактическим аргументом должно быть rvalue.


Если функция хранит копию аргумента, то разумный выбор между передачей по значению и набором перегрузок с помощью pass-by-ref-to-const и pass-by-rvalue-ref. Для выражения rvalue как фактического аргумента набор перегрузок может избежать одного хода. Это решение, основанное на инженерных решениях, заключается в том, что микро-оптимизация стоит дополнительной сложности и типизации.

Ответ 6

Что говорят rvalue об интерфейсе или копировании? rvalue предлагает вызывающему абоненту, что функция хочет иметь значение и не имеет намерения сообщить вызывающему абоненту о любых сделанных им изменениях. Рассмотрите следующее (я знаю, что вы не указали ссылки в своем примере, но несите меня):

//Hello. I want my own local copy of your Widget that I will manipulate,
//but I don't want my changes to affect the one you have. I may or may not
//hold onto it for later, but that none of your business.
void foo(Widget w);

//Hello. I want to take your Widget and play with it. It may be in a
//different state than when you gave it to me, but it'll still be yours
//when I'm finished. Trust me!
void foo(Widget& w);

//Hello. Can I see that Widget of yours? I don't want to mess with it;
//I just want to check something out on it. Read that one value from it,
//or observe what state it in. I won't touch it and I won't keep it.
void foo(const Widget& w);

//Hello. Ooh, I like that Widget you have. You're not going to use it
//anymore, are you? Please just give it to me. Thank you! It my
//responsibility now, so don't worry about it anymore, m'kay?
void foo(Widget&& w);

Для другого способа взглянуть на него:

//Here, let me buy you a new car just like mine. I don't care if you wreck
//it or give it a new paint job; you have yours and I have mine.
void foo(Car c);

//Here are the keys to my car. I understand that it may come back...
//not quite the same... as I lent it to you, but I'm okay with that.
void foo(Car& c);

//Here are the keys to my car as long as you promise to not give it a
//paint job or anything like that
void foo(const Car& c);

//I don't need my car anymore, so I'm signing the title over to you now.
//Happy birthday!
void foo(Car&& c);

Теперь, если виджеты должны оставаться уникальными (как фактические виджеты, скажем, GTK), то первый вариант не может работать. Второй, третий и четвертый варианты имеют смысл, потому что все еще существует только одно реальное представление данных. Во всяком случае, это то, что говорят эти семантики, когда я вижу их в коде.

Теперь, что касается эффективности: это зависит. Ссылки rvalue могут сэкономить много времени, если Widget имеет указатель на элемент данных, чей указательный контент может быть довольно большим (подумайте о массиве). Поскольку вызывающий использовал rvalue, они говорят, что им все равно, что они вам дают. Итак, если вы хотите переместить содержимое виджета вызывающего абонента в свой виджет, просто возьмите указатель. Не нужно тщательно копировать каждый элемент в структуре данных, на которую указывает указатель. Это может привести к довольно хорошим улучшениям скорости (опять же, думаю, массивы). Но если класс Widget не имеет такой вещи, этого преимущества нигде не видно.

Надеюсь, что это зависит от того, что вы просили; если нет, я могу, возможно, расширить/прояснить ситуацию.