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?