Зачем вам явно перемещать ссылку на пересылку?

Я просматриваю код, и вижу следующую функцию:

template <typename... Args>
static return_t make_return(Args &&... args)
{
    // using std::forward<Args> will preserve lvalue args as such, but the point of this function
    // is to make a return, where the 99.9+% case is moving a local (lvalue) into the return pack.
    // Thus it forces a move, which will move 'T&' args (but _not_ 'const T&' args) into the
    // return pack so that users don't need to type out a bunch of std::moves themselves/
    // If they don't want implicit move they can just call std::make_tuple directly
    return std::make_tuple(std::move(args)...);
}

Документация здесь меня смущает.
Зачем вам явно перемещать ссылку на пересылку?
Не хотите ли вы сохранить lvalue/rvalue в общем контексте?
У меня возникли проблемы с пониманием обоснования или того, как это поведение будет отличаться от рекомендованного std::forward.

Иными словами,
Я никогда не видел, чтобы кто-то явно отказывался от безупречной пересылки ссылки на пересылку.
Имеет ли это смысл?

Ответы

Ответ 1

Небольшой пример показывает намерение автора:

#include <tuple>
#include <iostream>

struct A {
  A() { }
  A(const A&) { std::cout << "copy\n"; }
  A(A&&) { std::cout << "move\n"; }
};

template <typename Arg>
static std::tuple<A> make_return(Arg&& arg) {
  return std::make_tuple(std::move(arg));
}

void f(const std::tuple<A>&) { }

void f1() {
  std::cout << "return local via make_tuple: ";
  A a{};
  f(std::make_tuple(a));
}

void f2() {
  std::cout << "return local via make_tuple(move): ";
  A a{};
  f(std::make_tuple(std::move(a)));
}

void f3() {
  std::cout << "return local via make_return: ";
  A a{};
  f(make_return(a));
}

void f4() {
  std::cout << "return const via make_tuple: ";
  const A a{};
  f(std::make_tuple(a));
}

void f5() {
  std::cout << "return const via make_tuple(move): ";
  const A a{};
  f(std::make_tuple(std::move(a)));
}

void f6() {
  std::cout << "return const via make_return: ";
  const A a{};
  f(make_return(a));
}

int main() {
  f1();
  f2();
  f3();
  f4();
  f5();
  f6();
}

Выход:

return local via make_tuple: copy
return local via make_tuple(move): move
return local via make_return: move
return const via make_tuple: copy
return const via make_tuple(move): copy
return const via make_return: copy

В случаях, когда возвращается локальная неконстантная переменная, мы хотим, чтобы std::move ее содержимое. Это можно std::make_tuple(std::move(a)) с помощью std::make_tuple(std::move(a)), потому что будет скопирован простой std::make_tuple(a). Чтобы сохранить некоторую типизацию, автор написал make_return как стенографию std::make_tuple(std::move(a)): пример показывает, что f3 работает так же, как f2.

Когда константа передается, std::move не имеет никакого значения, но и никакого вреда. Таким образом, мы могли бы использовать std::make_tuple, но make_return работает отлично. Случаи f4, f5, f6 все ведут себя одинаково, показывая, что не нужно думать дважды, прежде чем смешивать константы и не константы в make_return (в случае нескольких записей, составляющих return_t).

Остается перемещение неконстантной переменной, не являющейся локальной функцией, и поэтому мы не хотели бы разрушать ее содержимое. В этих случаях make_return нежелателен, и нужно вернуться к ручному вызову std::make_tuple (используя std::move если это необходимо).

Теперь, как это выглядит с помощью std::forward? Изменение определения make_return на использование

std::make_tuple(std::forward<Arg>(arg));

производит:

return local via tuple: copy
return local via tuple(move): move
return local via make_return: copy
return const via tuple: copy
return const via tuple(move): copy
return const via make_return: copy

так как a in f3 передается как const A&. Действительно, make_return тогда, по логике пересылки, является простым синонимом для std::move, теряя любую выгоду, которую мы надеялись достичь.

Ответ 2

Зачем вам явно перемещать ссылку на пересылку?

Потому что он не используется для своих свойств пересылки. Да, вы правы, мы обычно std::forward пересылаем ссылку на пересылку. Но в этом случае автор использовал пересылку ссылок исключительно в качестве удобства. Если это было написано как make_return(Args&... args) тогда было бы невозможно передать rvalue для make_return, так как ссылка не const const не может привязываться к одной.

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

Ответ 3

make_return() должен возвращать кортеж с использованием значения, так как это значение больше не понадобится, как это используется в make_return (конечная область функции a), нет необходимости использовать std::forward<>, так как это может пересылать ссылки lvalue, которые берутся в копиях (в зависимости от реализации), но ценность находится в конце области действия, поэтому не требуется для сохранения каких-либо ресурсов.

Принудительный std::move на make_tuple, сначала использует ссылки rvalue, опуская возможные дополнительные накладные расходы (в зависимости от реализации).