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 E
mieć 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_cast
jest to const
nieobecność:
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 deduce
funkcji i otrzymaliśmy wartość. Oznacza to, T
że A&
, a więc typem docelowym dla rzutowania statycznego jest A& &&
lub po prostu A&
. Ponieważ x
jest to już A&
, nic nie robimy i pozostaje nam odwołanie do wartości.
Kiedy już minął rvalue, T
jest 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 f
otrzymuje wartość, E
dostaje wartość. Po f
otrzymaniu wartości E
otrzymuje 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);
f
byłaby funkcją, a nie wyrażeniem?const int i
zostanie przyjęty:A
jest dedukowanyconst int
. Niepowodzenia dotyczą literałów wartości. Zauważ też, że dla wywołania dodeduced(1)
xint&&
nie jestint
(idealne przekazywanie nigdy nie tworzy kopii, tak jak by to było, gdybyx
był parametrem według wartości).T
Jest tylkoint
. Powodem,x
dla którego wartość przechodzi do wartości w forwarderze, jest to, że nazwane odwołania do wartości stają się wyrażeniami wartości.forward
lubmove
tutaj? Czy to tylko różnica semantyczna?std::move
powinien być wywoływany bez jawnych argumentów szablonu i zawsze powoduje wartość, podczas gdystd::forward
może skończyć się jako jeden z nich. Użyj,std::move
jeśli wiesz, że nie potrzebujesz już tej wartości i chcesz przenieść ją gdzie indziej, użyj,std::forward
aby to zrobić zgodnie z wartościami przekazanymi do szablonu funkcji.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
Funkcja
move
w kodzie tostd::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::forward
jest 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.Rzeczywiście, drukuje
Kod oparty jest na przykładzie z wcześniej wspomnianej dyskusji. Slajd 10, około 15:00 od początku.
źródło
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:
Teraz, jeśli nazywamy
outer
takchcielibyś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ż
t1
it2
w wyrażeniuinner(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,t1
inouter
jest zawsze wyrażeniem wartości, podczas gdyforward<T1>(t1)
może być wyrażeniem wartości w zależności odT1
. To ostatnie jest tylko wyrażeniem lvalue, jeśliT1
jest odwołaniem do lvalue. IT1
jest wyprowadzany tylko być lwartością odniesienia w przypadku, gdy pierwszy argument do zewnętrznej był lwartością wypowiedzi.źródło
Jeśli po utworzeniu wystąpienia
T1
jest on typuchar
iT2
należy do klasy, należy przekazaćt1
na kopię it2
naconst
odwołanie. No cóż, chyba żeinner()
weźmie się je bezconst
odniesienia, 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 zinner()
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, czyconst
wskaź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.
źródło
Kwestią, która nie została jeszcze wyjaśniona, jest to, że
static_cast<T&&>
radzi sobieconst T&
również poprawnie.Program:
Produkuje:
Zauważ, że „f” musi być funkcją szablonu. Jeśli jest to po prostu zdefiniowane jako „void f (int && a)”, to nie działa.
źródło
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?
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.
źródło