Има ли случаи на употреба за 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 приема универсална препратка?.

РЕДАКТИРАНЕ За да изясня въпроса, питам защо претоварване ( 2) от тук е необходимо и случай на употреба за него.


person vsoftco    schedule 25.04.2015    source източник
comment
Има случаи на използване за препращане на rvalue като rvalue. Вижте N2951.   -  person T.C.    schedule 25.04.2015
comment
Дали е за предотвратяване на препращането на rvalues ​​като lvalues ​​без използване на куп допълнителни условия 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
@T.C. благодаря, ще погледна.   -  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
Случай Б от 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. Използва се Overload 1, без него получавате грешка при компилация
  2. Използва се Overload 2, без него получавате грешка при компилиране
  3. Използва се Overload 1, без него получавате грешка при компилация
  4. Използва се Overload 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 също трябва да бъде дефиниран за rvalues

Накратко:

Тъй като без специализация на 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. Ще помисля за вашия пример, особено за направете копие напред и след това преместете. AFAIK, forward е просто кастинг с правила за свиване на препратки. Но във всеки случай, благодаря отново, скоро ще ви кажа какво мисля. - person vsoftco; 02.07.2015
comment
Добре, погледнете тук: coliru.stacked-crooked.com/a/1bce1b3c7e5d0237. Промених forward с моята реализация, наречена my_forward, която работи само за lстойности. Вашият код все още се компилира. Въпреки това, ако погледнете декларацията на std::forward, виждате, че има 2 претоварвания: (1) и (2). (2) приема prvovalue. Въпросът ми беше защо (2) е необходимо и какво е usege case? Както можете да видите, вашият код работи с my_forward, дефиниран само за lvalues. Има ли смисъл това, което питам? - person vsoftco; 02.07.2015
comment
Във вашия случай forward<vector<int>>(v) е същото като std::move(v) (вижте декларацията и правилата за свиване на препратки). Самото v все още е lvalue (има име). - person vsoftco; 03.07.2015
comment
Всъщност моят код използва първото претоварване, което исках да покажа, имахте предвид другото претоварване. Аналогично е, но с второто претоварване вие ​​приемате неща, които са изрично преместени (ако премахнете първото претоварване, кодът няма да се компилира, освен ако не преместите изрично с std::move, премахването на препратката прави невъзможно имплицитното преместване с 2-ро претоварване само) - person CoffeDeveloper; 03.07.2015
comment
вижте тук, не се компилира при преместване с другото претоварване само ideone.com/4Vvlnk и да, какво вие питането има много смисъл за мен - person CoffeDeveloper; 03.07.2015

Взирах се в този въпрос преди, прочетох връзката на Хауърд Хинант, не можах да го разбера напълно след час мислене. Сега търсих и получих отговора след пет минути. (Редактиране: полученият отговор е твърде щедър, тъй като връзката на Hinnant съдържаше отговора. Исках да кажа, че разбрах и успях да го обясня по по-прост начин, който се надявам някой да намери за полезен).

По принцип това ви позволява да бъдете общи в определени видове ситуации в зависимост от въведеното, което е предадено. Помислете за този код:

#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
Не си прилича освен повърхностно. Първоначално копирах и поставих вашия код в моята идея, защото бях любопитен. Тогава разбрах отговора и промених кода, който вече имах там. Не редактирах отговора ви, защото не е правилен, не би било незначителна модификация, за да го поправя. - person Nir Friedman; 03.07.2015