Есть ли варианты использования std::forward с prvalue?

Наиболее распространенное использование std::forward - это, ну, в общем, совершенная пересылка пересылаемой (универсальной) ссылки, например

template<typename T>
void f(T&& param)
{
    g(std::forward<T>(param)); // perfect forward to g
}

Здесь param — это lvalue, а std::forward в конечном итоге приводит его к rvalue или lvalue, в зависимости от того, какой аргумент ограничивал его.

Глядя на определение std::forward с сайта cppreference.com, я вижу, что есть также rvalue перегрузка

template< class T >
T&& forward( typename std::remove_reference<T>::type&& t );

Может ли кто-нибудь объяснить мне причину перегрузки rvalue? Я не вижу ни одного варианта использования. Если вы хотите передать rvalue функции, вы можете просто передать его как есть, не нужно применять к нему std::forward.

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

EDIT Чтобы прояснить вопрос, я спрашиваю, почему overload ( 2) отсюда необходимо и вариант использования для него.


person vsoftco    schedule 25.04.2015    source источник
comment
Существуют варианты использования для пересылки rvalue как rvalue. См. N2951.   -  person T.C.    schedule 25.04.2015
comment
Это сделано для того, чтобы предотвратить пересылку rvalue как lvalue без использования множества дополнительных условий enable_if? (см. случай C здесь)   -  person wakjah    schedule 25.04.2015
comment
@wakjah, как rvalue может быть перенаправлен как lvalue? Если вы вызываете f( prvalue ), где prvalue что-то вроде get_value(), то вы передаете rvalue, его не нужно пересылать. Может быть, я не понимаю, что вы имели в виду.   -  person vsoftco    schedule 25.04.2015
comment
@Т.С. спасибо, посмотрю.   -  person vsoftco    schedule 25.04.2015
comment
@vsoftco из того, что говорится на странице, на которую я ссылаюсь, нежелательный эффект, возможно, более точно описывается как пересылка rvalue в lvalue или сохранение ссылки lvalue на rvalue, что делают некоторые возможные реализации std::forward легче сделать, чем другие   -  person wakjah    schedule 25.04.2015
comment
Почти дубликат этого вопроса, хотя в самом вопросе конкретно не задается вопрос о перегрузке rvalue. Ховард говорит о причинах этого, и у меня есть очень надуманный пример, который показывает это в действии.   -  person Praetorian    schedule 25.04.2015
comment
@Praetorian, да, я видел связанный вопрос, я понимаю, как это работает, через ссылку. сворачивая правила, я просто не смог придумать хороший вариант использования перегрузки rvalue.   -  person vsoftco    schedule 25.04.2015
comment
Случай B из open-std.org/jtc1 /sc22/wg21/docs/papers/2009/n2951.html — вот ответ.   -  person Howard Hinnant    schedule 25.04.2015
comment
@HowardHinnant, спасибо!   -  person vsoftco    schedule 25.04.2015
comment
обман? stackoverflow.com/questions/29041246 /   -  person evan    schedule 26.04.2015
comment
@evan почти ... Я написал Скотту Мейерсу по электронной почте об этой проблеме, и его ответ заключался в том, что перегрузка rvalue просто исключена из книги, поскольку он не мог придумать для нее реального варианта использования. Вот почему я спросил здесь о реальном случае использования.   -  person vsoftco    schedule 26.04.2015
comment
Для идеальной переадресации? Допустим, вы реализуете шаблон фабричной функции, который возвращает shared_ptr‹T›, основываясь на том факте, что rvalue reference не является rvalue. Я не уверен, что это то, о чем вы спрашиваете.   -  person Dean Seo    schedule 02.06.2015
comment
да, фабричный шаблон, возвращающий shared_ptr‹T›, является прекрасным примером, я использовал его уже 2 года назад в своей структуре (прямая ссылка здесь на фабричную функцию (строка 80): github.com/Darelbi/Infectorpp/blob/master/include/Infectorpp/ удивительно, что Скотт Мейерс не подумал об этом .   -  person CoffeDeveloper    schedule 02.07.2015
comment
@DarioOO спасибо за ссылку. Можешь написать краткий ответ? Из вашего примера мне до сих пор не ясно, почему std::forward нужно также определять для rvalues.   -  person vsoftco    schedule 02.07.2015
comment
Ответил в моем комментарии к моему ответу, должен ли я отредактировать комментарий обратно в ответ, чтобы его приняли? :)   -  person CoffeDeveloper    schedule 03.07.2015
comment
До сих пор вы решили проблему? Я думаю, что сейчас есть более одного ответа на это :)   -  person CoffeDeveloper    schedule 09.07.2015
comment
@DarioOO Извините, в последнее время был занят другими делами. Посмотрю и, конечно, если ответ (ы) на проблему будет принят.   -  person vsoftco    schedule 10.07.2015
comment
Один вопрос к вам: почему вы вызываете f для пересылки аргументов в g, а не вызываете g напрямую?   -  person mercury0114    schedule 30.07.2020


Ответы (3)


Хорошо, поскольку @vsoftco запросил краткий вариант использования, вот усовершенствованная версия (используя его идею наличия «my_forward», чтобы фактически увидеть, какая перегрузка вызывается).

Я интерпретирую «вариант использования», предоставляя пример кода, который без prvalue не компилируется или ведет себя по-другому (независимо от того, было бы это действительно полезно или нет).

У нас есть 2 перегрузки для std::forward

#include <iostream>

template <class T>
inline T&& my_forward(typename std::remove_reference<T>::type& t) noexcept
{
    std::cout<<"overload 1"<<std::endl;
    return static_cast<T&&>(t);
}

template <class T>
inline T&& my_forward(typename std::remove_reference<T>::type&& t) noexcept
{
    std::cout<<"overload 2"<<std::endl;
    static_assert(!std::is_lvalue_reference<T>::value,
              "Can not forward an rvalue as an lvalue.");
    return static_cast<T&&>(t);
}

И у нас есть 4 возможных варианта использования

Сценарий 1

#include <vector>
using namespace std;

class Library
{
    vector<int> b;
public:
    // &&
    Library( vector<int>&& a):b(std::move(a)){

    }
};

int main() 
{
    vector<int> v;
    v.push_back(1);
    Library a( my_forward<vector<int>>(v)); // &
    return 0;
}

Сценарий 2

#include <vector>
using namespace std;

class Library
{
    vector<int> b;
public:
    // &&
    Library( vector<int>&& a):b(std::move(a)){

    }
};

int main() 
{
    vector<int> v;
    v.push_back(1);
    Library a( my_forward<vector<int>>(std::move(v))); //&&
    return 0;
}

Сценарий 3

#include <vector>
using namespace std;

class Library
{
    vector<int> b;
public:
    // &
    Library( vector<int> a):b(a){

    }
};

int main() 
{
    vector<int> v;
    v.push_back(1);
    Library a( my_forward<vector<int>>(v)); // &
    return 0;
}

Сценарий 4

#include <vector>
using namespace std;

class Library
{
    vector<int> b;
public:
    // &
    Library( vector<int> a):b(a){

    }
};

int main() 
{
    vector<int> v;
    v.push_back(1);
    Library a( my_forward<vector<int>>(std::move(v))); //&&
    return 0;
}

Вот резюме

  1. Используется перегрузка 1, без нее вы получите ошибку компиляции
  2. Используется перегрузка 2, без нее вы получите ошибку компиляции
  3. Используется перегрузка 1, без нее вы получите ошибку компиляции
  4. Используется перегрузка 2, без нее вы получите ошибку компиляции

Обратите внимание, что если мы не используем переадресацию

Library a( std::move(v));
//and
Library a( v);

ты получаешь:

  1. Ошибка компиляции
  2. Скомпилировать
  3. Компиляция
  4. Скомпилировать

Как видите, если вы используете только одну из двух перегрузок forward, вы в основном заставляете не компилировать 2 случая из 4, а если вы вообще не используете forward, вы можете скомпилировать только 3 случая из 4.

person CoffeDeveloper    schedule 03.07.2015
comment
думая о сложности переадресации, я бы назвал это несовершенной переадресацией. - person CoffeDeveloper; 03.07.2015

Этот ответ предназначен для ответа на комментарий @vsoftco

@DarioOO спасибо за ссылку. Можешь написать краткий ответ? Из вашего примера мне до сих пор не ясно, почему std::forward также должен быть определен для значений rvalue

Вкратце:

Потому что без специализации rvalue следующий код не скомпилировался бы

#include <utility>
#include <vector>
using namespace std;

class Library
{
    vector<int> b;
public:
    // hi! only rvalue here :)
    Library( vector<int>&& a):b(std::move(a)){

    }
};

int main() 
{
    vector<int> v;
    v.push_back(1);
    A a( forward<vector<int>>(v));
    return 0;
}

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

Длинная версия:

Вам нужно переместить v, потому что класс Library не имеет конструктора, принимающего к нему lvalue, а только ссылку на rvalue. Без идеальной переадресации мы оказались бы в нежелательном поведении:

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

с семантикой перемещения мы гарантируем, что конструктор перемещения используется, ЕСЛИ ВОЗМОЖНО. В приведенном выше примере, если мы удалим std::forward, код не скомпилируется.

Так что же на самом деле делает forward? перемещение элемента без нашего согласия? Неа!

Он просто создает копию вектора и перемещает ее. Как мы можем быть в этом уверены? Просто попробуйте получить доступ к элементу.

vector<int> v;
v.push_back(1);
A a( forward<vector<int>>(v)); //what happens here? make a copy and move
std::cout<<v[0];     // OK! std::forward just "adapted" our vector

если вы вместо этого переместите этот элемент

vector<int> v;
v.push_back(1);
A a( std::move(v)); //what happens here? just moved
std::cout<<v[0];  // OUCH! out of bounds exception

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

На самом деле следующий код просто не скомпилируется:

vector<int> v;
v.push_back(1);
A a( v); //try to copy, but not find a lvalue constructor

Реальный вариант использования:

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

template< typename Impl, typename... SmartPointers>
static std::shared_ptr<void> 
    instancesFactoryFunction( priv::Context * ctx){
        return std::static_pointer_cast<void>( std::make_shared<Impl>(

                std::forward< typename SmartPointers::pointerType>( 
            SmartPointers::resolve(ctx))... 
            )           );
}

Код был взят из моего фреймворка (строка 80): Infectorpp 2

В этом случае аргументы пересылаются из вызова функции. Возвращаемые значения SmartPointers::resolve перемещаются правильно, независимо от того, что конструктор Impl принимает rvalue или lvalue (поэтому нет ошибок компиляции, и они все равно перемещаются).

По сути, вы можете использовать std::foward в любом случае, когда хотите сделать код проще и читабельнее, но вы должны помнить 2 момента.

  • дополнительное время компиляции (не так много на самом деле)
  • может вызвать нежелательные копии (когда вы явно не перемещаете что-то во что-то, что требует rvalue)

Если использовать с осторожностью, это мощный инструмент.

person CoffeDeveloper    schedule 02.07.2015
comment
Спасибо, кстати, у вас должно быть Library вместо A. Я подумаю над вашим примером, особенно над созданием копии, а затем перемещением. Насколько я знаю, forward просто использует правила свертывания ссылок. Но в любом случае, еще раз спасибо, скоро дам вам знать, что я думаю. - person vsoftco; 02.07.2015
comment
Хорошо, посмотрите здесь: coliru.stacked-crooked.com/a/1bce1b3c7e5d0237. Я изменил forward на свою реализацию под названием my_forward, которая работает только для lvalue. Ваш код все еще компилируется. Однако, если вы посмотрите на декларацию std::forward, вы увидите, что есть 2 перегрузки: (1) и (2). (2) принимает значение. Мой вопрос заключался в том, почему (2) необходимо и что такое вариант использования? Как видите, ваш код работает с my_forward, определенным только для lvalue. Имеет ли смысл то, что я спрашиваю? - person vsoftco; 02.07.2015
comment
В вашем случае forward<vector<int>>(v) совпадает с std::move(v) (см. объявление и правила свертывания ссылок). v сам по себе по-прежнему является lvalue (имеет имя). - person vsoftco; 03.07.2015
comment
На самом деле, мой код использует первую перегрузку, которую я хотел показать, вы имели в виду другую перегрузку. Это аналогично, но со 2-й перегрузкой вы принимаете материал, который явно перемещен (если вы удалите первую перегрузку, код не будет компилироваться, если вы не переместите явно с помощью std::move, удаление ссылки сделает невозможным неявное перемещение со 2-й перегрузкой Только) - person CoffeDeveloper; 03.07.2015
comment
посмотрите здесь, он не компилируется при перемещении с другой перегрузкой, только ideone.com/4Vvlnk и да, что вы вопрос имеет для меня большой смысл - person CoffeDeveloper; 03.07.2015

Я смотрел на этот вопрос раньше, читал ссылку Говарда Хиннанта, не мог полностью понять ее после часа размышлений. Сейчас поискал и через пять минут получил ответ. (Редактировать: получил ответ слишком щедр, так как в ссылке Хиннанта был ответ. Я имел в виду, что понял и смог объяснить это более простым способом, который, надеюсь, кому-то покажется полезным).

По сути, это позволяет вам быть универсальным в определенных ситуациях в зависимости от переданного типа. Рассмотрим этот код:

#include <utility>
#include <vector>
#include <iostream>
using namespace std;

class GoodBye
{
  double b;
 public:
  GoodBye( double&& a):b(std::move(a)){ std::cerr << "move"; }
  GoodBye( const double& a):b(a){ std::cerr << "copy"; }
};

struct Hello {
  double m_x;

  double & get()  { return m_x; }
};

int main()
{
  Hello h;
  GoodBye a(std::forward<double>(std::move(h).get()));
  return 0;
}

Этот код печатает «перемещение». Что интересно, если я удалю std::forward, он напечатает копию. Для меня это трудно осознать, но давайте примем это и будем двигаться дальше. (Редактировать: я полагаю, это происходит потому, что get вернет ссылку lvalue на rvalue. Такой объект распадается на lvalue, но std::forward преобразует его в rvalue, как и при обычном использовании forward. Все еще кажется неинтуитивным хоть).

Теперь давайте представим другой класс:

struct Hello2 {
  double m_x;

  double & get() & { return m_x; }
  double && get() && { return std::move(m_x); }
};

Предположим, что в коде main h был экземпляром Hello2. Теперь нам больше не нужен std::forward, потому что вызов std::move(h).get() возвращает rvalue. Однако предположим, что код является общим:

template <class T>
void func(T && h) {
  GoodBye a(std::forward<double>(std::forward<T>(h).get()));
}

Теперь, когда мы вызываем func, мы хотим, чтобы он правильно работал как с Hello, так и с Hello2, то есть мы хотели бы инициировать перемещение. Это происходит только для rvalue Hello, если мы включаем внешнее std::forward, поэтому оно нам нужно. Но... Мы добрались до кульминации. Когда мы передаем rvalue Hello2 этой функции, перегрузка rvalue функции get() уже вернет rvalue double, поэтому std::forward фактически принимает rvalue. Поэтому, если бы это было не так, вы не смогли бы написать полностью общий код, как указано выше.

Проклятие.

person Nir Friedman    schedule 03.07.2015
comment
Если ответ - ваша работа, почему у вас есть код, похожий на мой ответ? (не говоря уже о том, что вы включаете <vector>, как в моем ответе, но не используете его: / чувствуется, что вы очень много копируете и редактируете, так почему бы вместо этого не редактировать напрямую? ).. - person CoffeDeveloper; 03.07.2015
comment
Это не похоже, кроме как внешне. Сначала я скопировал и вставил ваш код в свой ide, потому что мне было любопытно. Затем я понял ответ и изменил код, который у меня уже был. Я не редактировал ваш ответ, потому что он неверен, исправить его было бы незначительной модификацией. - person Nir Friedman; 03.07.2015