Jakie są główne cele używania std :: forward i jakie problemy rozwiązuje?

438

W doskonałym przekazywaniu std::forwardsłuży do konwersji nazwanych odwołań do wartości t1i t2do nienazwanych odwołań do wartości. Jaki jest tego cel? Jak wpłynęłoby to na wywoływaną funkcję inner, gdybyśmy opuścili wartości t1& t2jako wartości?

template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}
Steveng
źródło

Odpowiedzi:

789

Musisz zrozumieć problem z przekazywaniem. Możesz przeczytać cały problem szczegółowo , ale streszczę.

Zasadniczo, biorąc pod uwagę wyrażenie E(a, b, ... , c), chcemy, aby wyrażenie f(a, b, ... , c)było równoważne. W C ++ 03 jest to niemożliwe. Jest wiele prób, ale wszystkie pod pewnym względem kończą się niepowodzeniem.


Najprostszym jest użycie odwołania do wartości:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
    E(a, b, c);
}

Ale to nie obsługuje wartości tymczasowych: f(1, 2, 3);ponieważ nie można ich powiązać z odwołaniem do wartości.

Następna próba może być:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(a, b, c);
}

Co rozwiązuje powyższy problem, ale odwraca klapy. Teraz nie pozwala Emieć argumentów nie stałych:

int i = 1, j = 2, k = 3;
void E(int&, int&, int&); f(i, j, k); // oops! E cannot modify these

Trzecia próba akceptuje const-referencje, ale const_castjest to constnieobecność:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}

To akceptuje wszystkie wartości, może przekazywać wszystkie wartości, ale potencjalnie prowadzi do nieokreślonego zachowania:

const int i = 1, j = 2, k = 3;
E(int&, int&, int&); f(i, j, k); // ouch! E can modify a const object!

Ostateczne rozwiązanie obsługuje wszystko poprawnie ... kosztem niemożności utrzymania. Podać przeciążeniem f, ze wszystkimi kombinacjami const i non-const:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);

N argumentów wymaga 2 N kombinacji, koszmar. Chcielibyśmy to zrobić automatycznie.

(Właśnie to kompilator robi dla nas w C ++ 11).


W C ++ 11 mamy szansę to naprawić. Jedno rozwiązanie modyfikuje reguły dedukcji szablonów dla istniejących typów, ale potencjalnie psuje to wiele kodu. Musimy więc znaleźć inny sposób.

Rozwiązaniem jest zamiast tego użyć nowo dodanych odwołań do wartości ; możemy wprowadzić nowe reguły podczas dedukcji typów referencyjnych wartości i stworzyć pożądany rezultat. W końcu nie możemy teraz złamać kodu.

Jeśli podano odniesienie do odwołania (odniesienie do notatki jest obejmującym terminem oznaczającym jednocześnie T&i T&&), stosujemy następującą regułę, aby ustalić wynikowy typ:

„[biorąc pod uwagę] typ TR, który jest odwołaniem do typu T, próba utworzenia„ odniesienia do wartości lvue do cv TR ”powoduje utworzenie„ odniesienia do lvalue do T ”, a próba utworzenia„ odniesienia do lvalue do T ” cv TR ”tworzy typ TR.”

Lub w formie tabelarycznej:

TR   R

T&   &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&   && -> T&  // rvalue reference to cv TR -> TR (lvalue reference to T)
T&&  &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&&  && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)

Następnie, po odjęciu argumentu szablonu: jeśli argument jest wartością A, dostarczamy argumentowi szablonu odwołanie do wartości A. W przeciwnym razie dedukujemy normalnie. Daje to tak zwane uniwersalne referencje (termin referencyjny przekazujący jest teraz oficjalny).

Dlaczego to jest przydatne? Ponieważ w połączeniu utrzymujemy możliwość śledzenia kategorii wartości typu: jeśli była to wartość, mamy parametr odniesienia do wartości, w przeciwnym razie mamy parametr odniesienia do wartości.

W kodzie:

template <typename T>
void deduce(T&& x); 

int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)

Ostatnią rzeczą jest „przekazanie” kategorii wartości zmiennej. Pamiętaj, że po wejściu do funkcji parametr można przekazać jako wartość do dowolnej wartości:

void foo(int&);

template <typename T>
void deduce(T&& x)
{
    foo(x); // fine, foo can refer to x
}

deduce(1); // okay, foo operates on x which has a value of 1

To nie dobrze. E musi mieć taką samą kategorię wartości, jak my! Rozwiązanie jest następujące:

static_cast<T&&>(x);

Co to robi? Rozważmy, że jesteśmy w tej deducefunkcji i otrzymaliśmy wartość. Oznacza to, Tże A&, a więc typem docelowym dla rzutowania statycznego jest A& &&lub po prostu A&. Ponieważ xjest to już A&, nic nie robimy i pozostaje nam odwołanie do wartości.

Kiedy już minął rvalue, Tjest A, więc typ docelowy dla obsady jest statycznym A&&. Rzutowanie powoduje wyrażenie wartości, które nie może być dłużej przekazywane do odwołania do wartości . Zachowaliśmy kategorię wartości parametru.

Złożenie ich razem daje nam „doskonałe przekazywanie”:

template <typename A>
void f(A&& a)
{
    E(static_cast<A&&>(a)); 
}

Kiedy fotrzymuje wartość, Edostaje wartość. Po fotrzymaniu wartości Eotrzymuje wartość. Doskonały.


I oczywiście chcemy pozbyć się brzydoty. static_cast<T&&>jest tajemniczy i dziwny do zapamiętania; stwórzmy zamiast tego funkcję narzędziową forward, która robi to samo:

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
GManNickG
źródło
1
Czy nie fbyłaby funkcją, a nie wyrażeniem?
Michael Foukarakis
1
Twoja ostatnia próba nie jest poprawna w odniesieniu do opisu problemu: Prześle wartości const jako non-const, a tym samym w ogóle nie przekieruje. Zauważ też, że w pierwszej próbie const int izostanie przyjęty: Ajest dedukowany const int. Niepowodzenia dotyczą literałów wartości. Zauważ też, że dla wywołania do deduced(1)x int&&nie jest int(idealne przekazywanie nigdy nie tworzy kopii, tak jak by to było, gdyby xbył parametrem według wartości). TJest tylko int. Powodem, xdla którego wartość przechodzi do wartości w forwarderze, jest to, że nazwane odwołania do wartości stają się wyrażeniami wartości.
Johannes Schaub - litb
5
Czy jest jakaś różnica w użyciu forwardlub movetutaj? Czy to tylko różnica semantyczna?
0x499602D2
28
@David: std::movepowinien być wywoływany bez jawnych argumentów szablonu i zawsze powoduje wartość, podczas gdy std::forwardmoże skończyć się jako jeden z nich. Użyj, std::movejeśli wiesz, że nie potrzebujesz już tej wartości i chcesz przenieść ją gdzie indziej, użyj, std::forwardaby to zrobić zgodnie z wartościami przekazanymi do szablonu funkcji.
GManNickG
5
Dziękujemy za rozpoczęcie od konkretnych przykładów i motywowanie problemu; bardzo pomocny!
ShreevatsaR
61

Myślę, że kod koncepcyjny implementujący std :: forward może dodać do dyskusji. Oto slajd z przemówienia Scotta Meyersa An Effective C ++ 11/14 Sampler

kod koncepcyjny implementujący std :: forward

Funkcja movew kodzie to std::move. Istnieje wcześniej (działająca) implementacja tego wcześniej w tym wykładzie. Znalazłem rzeczywistą implementację std :: forward w libstdc ++ , w pliku move.h, ale nie jest to wcale pouczające.

Z perspektywy użytkownika oznacza to, że std::forwardjest to warunkowa obsada wartości. Może być przydatny, gdy piszę funkcję, która oczekuje wartości lub wartości w parametrze i chce przekazać ją innej funkcji jako wartość tylko wtedy, gdy została przekazana jako wartość. Gdybym nie zawinął parametru w std :: forward, zawsze byłby przekazywany jako normalne odwołanie.

#include <iostream>
#include <string>
#include <utility>

void overloaded_function(std::string& param) {
  std::cout << "std::string& version" << std::endl;
}
void overloaded_function(std::string&& param) {
  std::cout << "std::string&& version" << std::endl;
}

template<typename T>
void pass_through(T&& param) {
  overloaded_function(std::forward<T>(param));
}

int main() {
  std::string pes;
  pass_through(pes);
  pass_through(std::move(pes));
}

Rzeczywiście, drukuje

std::string& version
std::string&& version

Kod oparty jest na przykładzie z wcześniej wspomnianej dyskusji. Slajd 10, około 15:00 od początku.

użytkownik7610
źródło
2
Twój drugi link ostatecznie wskazał coś zupełnie innego.
Pharap
34

W doskonałym przekazywaniu std :: forward służy do konwersji nazwanego odwołania do wartości t1 i t2 na nienazwane odwołanie do wartości. Jaki jest tego cel? Jak wpłynęłoby to na wywołaną funkcję wewnętrzną, gdybyśmy pozostawili t1 i t2 jako wartość?

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

Jeśli użyjesz nazwanego odwołania do wartości w wyrażeniu, jest to faktycznie wartość (ponieważ odwołujesz się do obiektu po nazwie). Rozważ następujący przykład:

void inner(int &,  int &);  // #1
void inner(int &&, int &&); // #2

Teraz, jeśli nazywamy outertak

outer(17,29);

chcielibyśmy, aby 17 i 29 zostały przekazane do # 2, ponieważ 17 i 29 są literałami całkowitymi i jako takie są wartościami. Ale ponieważ t1i t2w wyrażeniu inner(t1,t2);są lwartościami, byłbyś powołując # 1 zamiast # 2. Dlatego musimy przekształcić odniesienia z powrotem w odniesienia nienazwane za pomocą std::forward. Tak więc, t1in outerjest zawsze wyrażeniem wartości, podczas gdy forward<T1>(t1)może być wyrażeniem wartości w zależności od T1. To ostatnie jest tylko wyrażeniem lvalue, jeśli T1jest odwołaniem do lvalue. I T1jest wyprowadzany tylko być lwartością odniesienia w przypadku, gdy pierwszy argument do zewnętrznej był lwartością wypowiedzi.

sellibitze
źródło
Jest to rodzaj rozwodnionego wyjaśnienia, ale bardzo dobrze wykonane i funkcjonalne wyjaśnienie. Ludzie powinni najpierw przeczytać tę odpowiedź, a następnie w razie potrzeby zejść głębiej
NicoBerrogorry
@sellibitze Jeszcze jedno pytanie, które stwierdzenie jest właściwe przy dedukcji int a; f (a): „ponieważ a jest wartością, więc int (T&&) oznacza int (int & &&)” lub „aby T&& równało się int &, więc T powinno być int & "? Wolę ten drugi.
John
11

Jak wpłynęłoby to na wywoływaną funkcję wewnętrzną, gdybyśmy pozostawili t1 i t2 jako wartość?

Jeśli po utworzeniu wystąpienia T1jest on typu chari T2należy do klasy, należy przekazać t1na kopię i t2na constodwołanie. No cóż, chyba że inner()weźmie się je bez constodniesienia, to znaczy, w którym przypadku też chcesz to zrobić.

Spróbuj napisać zestaw outer()funkcji, które implementują to bez odwołań do wartości, dedukując właściwy sposób przekazywania argumentów z inner()typu. Myślę, że będziesz potrzebować czegoś 2 ^ 2, dość mocnych szablonów-meta, aby wydedukować argumenty, i dużo czasu, aby to zrobić we wszystkich przypadkach.

A potem ktoś przychodzi z inner()argumentem, który bierze argument za wskaźnik. Myślę, że teraz daje 3 ^ 2. (Lub 4 ^ 2. Do diabła, nie przejmuję się próbą wymyślenia, czy constwskaźnik miałby znaczenie.)

A potem wyobraź sobie, że chcesz to zrobić dla pięciu parametrów. Lub siedem.

Teraz już wiesz, dlaczego niektóre jasne umysły wymyśliły „idealne przekazywanie”: To sprawia, że ​​kompilator robi to wszystko za Ciebie.

sbi
źródło
5

Kwestią, która nie została jeszcze wyjaśniona, jest to, że static_cast<T&&>radzi sobie const T&również poprawnie.
Program:

#include <iostream>

using namespace std;

void g(const int&)
{
    cout << "const int&\n";
}

void g(int&)
{
    cout << "int&\n";
}

void g(int&&)
{
    cout << "int&&\n";
}

template <typename T>
void f(T&& a)
{
    g(static_cast<T&&>(a));
}

int main()
{
    cout << "f(1)\n";
    f(1);
    int a = 2;
    cout << "f(a)\n";
    f(a);
    const int b = 3;
    cout << "f(const b)\n";
    f(b);
    cout << "f(a * b)\n";
    f(a * b);
}

Produkuje:

f(1)
int&&
f(a)
int&
f(const b)
const int&
f(a * b)
int&&

Zauważ, że „f” musi być funkcją szablonu. Jeśli jest to po prostu zdefiniowane jako „void f (int && a)”, to nie działa.

lalawawa
źródło
Dobra uwaga, więc T&& w obsadzie statycznej jest również zgodny z regułami zwijania odniesienia, prawda?
barney
3

Warto podkreślić, że forward musi być stosowany w połączeniu z zewnętrzną metodą z przekazywaniem / uniwersalnym odniesieniem. Używanie samego przodu jako poniższych instrukcji jest dozwolone, ale nie przynosi niczego poza wprowadzaniem zamieszania. Komitet standardowy może chcieć wyłączyć taką elastyczność, w przeciwnym razie dlaczego nie użyjemy zamiast tego static_cast?

     std::forward<int>(1);
     std::forward<std::string>("Hello");

Moim zdaniem przesuwanie w przód i w przód to wzorce projektowe, które są naturalnymi rezultatami po wprowadzeniu typu odniesienia wartości r. Nie powinniśmy nazwać metody zakładającej, że jest ona właściwie używana, chyba że nieprawidłowe użycie jest zabronione.

Colin
źródło
Nie sądzę, że komitet C ++ uważa, że ​​to na nich spoczywa obowiązek „poprawnego” używania idiomów językowych, ani nawet definiowania, czym jest „prawidłowe” użycie (chociaż z pewnością mogą podać wytyczne). W tym celu, podczas gdy nauczyciele, szefowie i przyjaciele danej osoby mogą mieć obowiązek kierowania nimi w taki czy inny sposób, uważam, że komitet C ++ (a zatem standard) nie ma tego obowiązku.
SirGuy,
Tak, właśnie przeczytałem N2951 i zgadzam się, że komitet standardowy nie ma obowiązku dodawania niepotrzebnych ograniczeń dotyczących korzystania z funkcji. Ale nazwy tych dwóch szablonów funkcyjnych (przesuwanie i przewijanie do przodu) są rzeczywiście nieco mylące, ponieważ tylko ich definicje znajdują się w pliku biblioteki lub standardowej dokumentacji (23.2.5 pomocniki przewijania / przewijania). Przykłady w standardzie zdecydowanie pomagają zrozumieć tę koncepcję, ale przydatne może być dodanie dodatkowych uwag, aby uczynić wszystko nieco jaśniejszym.
colin,