Problemem do rozwiązania jest napisanie funkcji f, która wywołuje
funkcję g i doskonale przekazuje jej swój argument. Doskonale,
czyli:
bez kopiowania argumentu,
z zachowaniem własności argumentu.
O typie parametru (w tym kwalifikatorach const i volatile) funkcji
g nic nie wiemy: może być dowolny. Funkcja g może mieć też
przeciążenia i przeciążone szablony. Chcemy napisać tylko jedną
implementację funkcji f, a więc musimy zaimplementować szablon
funkcji, żeby pozwolić na przyjmowanie argumentów dowolnego typu. Ten
problem nazywamy doskonałym przekazywaniem argumentu (ang. perfect
argument forwarding).
Musimy zachować własności argumentu, żeby wyrażenie f(<expr>)
wywołało to samo przeciążenie funkcji g co wyrażenie g(<expr>).
Problem sprowadza się do zachowania kategorii i typu
przekazywanego argumentu.
Jest to ujęcie problemu od C++11, ponieważ mowa o zachowaniu kategorii
argumentu: jeżeli funkcja f otrzymała r-wartość (albo l-wartość), to
powinna przekazać do funkcji g też r-wartość (albo l-wartość).
Należy zachować kategorię, ponieważ r-wartość może wymagać specjalnego
traktowania (chodzi o przenoszenie wartości).
Problem też istniał w starym C++ (przed C++11), ale kategoria wartości
nie miała wpływu na wybór przeciążenia i nie dało się jej zachować
podczas przekazywania. Wówczas chodziło o zachowanie wyboru jednego z
dwóch przeciążeń funkcji g dla parametru typu referencyjnego (a
dokładnie l-referencji):
niestałego, np. void g(A &);,
stałego, np. void g(const A &);.
Zadaniem jest napisanie takiego szablonu funkcji:
template<typename T>
void
f(qualifiers_a type_a a)
{
g(a); // Is calling like this enough?
}
PYTANIE: Czy można napisać taki szablon funkcji f? Jakie mają
być kwalifikatory qualifiers_a i jaki typ type_a? Czy
kwalifikatorem może, czy musi być const? Czy typem ma być T, T
&, czy T &&?
ODPOWIEDŹ: Można, ale tylko od C++11, ponieważ tylko od C++11 zachowanie kategorii ma znaczenie.
Zadanie jest problematyczne, bo argumentem wywołania funkcji może być albo l-wartość, albo r-wartość. Są dwa podproblemy.
Problemem jest określenie typu parametru funkcji, żeby mógł on być zawsze zainicjalizowany, bez względu na typ i kategorię argumentu. Ten podproblem już jest częściowo rozwiązany przez użycie szablonu funkcji, ponieważ wnioskowanie argumentów szablonu nie dopuszcza do konwersji typów.
Problemem jest utrata kategorii argumentu. W ciele funkcji, wyrażenie
z nazwą parametru funkcji jest zawsze l-wartością, nawet jeżeli
parametr jest r-referencją (która została zainicjalizowana
r-wartością). Zachowanie kategorii argumentu funkcji f podczas
przekazywania go do funkcji g ma znaczenie, bo też kategoria (a nie
tylko typ) argumentu ma wpływ na wybór przeciążenia funkcji g.
Funkcje szablonowe std::make_unique i std::make_shared są
fabrykami obiektów. Tworzą one obiekty i muszą przekazać swoje
argumenty do konstruktora klasy w niezmienionej postaci.
To jest przykład dla dwóch parametrów:
template<typename T, typename A1, typename A2>
unique_ptr<T>
make_unique(qualifiers_a1 type_a1 a1,
qualifiers_a2 type_a2 a2)
{
return unique_ptr<T>(new T(a1, a2));
}
void foo(int x);, gdzie x jest parametrem funkcjifoo(a);, gdzie a jest argumentem wywołania funkcjiArgument może być l-wartością albo r-wartością, a parametr zawsze jest l-wartością, bo ma nazwę (możemy pobrać jego adres).
Możliwe rozwiązania z pominięciem kwalifikatora volatile.
Tconst TT &const T &T &&const T &&Nie bierzemy pod uwagę rozwiązań:
const T, bo jest to przekazanie przez wartość, jak w przypadku
T, z deklaracją, że parametru nie będziemy zmieniać,
const T &&, bo nie znam zastosowania tego typu referencji.
TWygląda tak:
template<typename T>
void
f(T t)
{
g(t);
}
Gdy wykonamy f(1), a funkcja g będzie pobierać argumenty przez
referencję, to nie otrzyma referencji na oryginalny obiekt, a
referencję na parametr funkcji f, który jest kopią oryginalnego
obiektu. Złe rozwiązanie.
Zatem zostają nam trzy przypadki z referencjami do rozważenia:
T &const T &T &&T &Wygląda tak:
template<typename T>
void
f(T &t)
{
g(t);
}
Jeżeli argumentem wywołania funkcji f jest r-wartość, to kompilacja
nie powiedzie się, bo l-referencja nie może być zainicjalizowana
r-wartością. Złe rozwiązanie.
Przykład:
void g(const int &)
{
}
template <typename T>
void f(T &t)
{
g(t);
}
struct A
{
};
int main()
{
// We can call "g" all right with an rvalue.
g(1);
// But we cannot call "f" with an rvalue.
// f(1);
// This doesn't compile either.
// f(A());
}
const T &Wygląda tak:
template<typename T>
void
f(const T &t)
{
g(t);
}
Teraz będzie się kompilować dla r-wartości, np. f(1), ale jeżeli
parametr funkcji g będzie niestałą l-referencją, to kod nie będzie
się kompilował, bo niestałej l-referencji nie można zainicjalizować
stałą referencją. Złe rozwiązanie.
Przykład:
void g(int &)
{
}
template <typename T>
void f(const T &t)
{
g(t);
}
int main()
{
int x = 1;
// We can call "g" with an lvalue of non-const type.
g(x);
// We cannot call "f" with an lvalue of non-cost, because in "f"
// it's bound to a const lvalue reference, which cannot be used to
// initialize the parameter of g, which is a non-const lvalue
// reference.
// f(x);
}
T & razem z const T &Możemy mieć dwa przciążenia szablonów podstawowych: jeden dla T &, a
drugi dla const T &:
template<typename T>
void
f(T &t)
{
g(t);
}
template<typename T>
void
f(const T &t)
{
g(t);
}
Ta implementacja rozwiąże podproblem #1, ale dla n parametrów
potrzebujemy 2^n przeciążeń szablonów podstawowych! W starym C++
było to jedyne możliwe rozwiązanie, więc wówczas było akceptowalne.
Kompatybilność wstecz jest zachowana: kompilator C++11 będzie
poprawnie kompilował stary kod (bez r-referencji).
Jednak w C++11 to rozwiązanie nie jest w stanie doskonale przekazać r-wartości, bo nie uwzględnia ono przeciążenia z r-referencją. Od C++11 jest to złe rozwiązanie.
Przykład:
#include <iostream>
void g(int &)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
void g(const int &)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
void g(int &&)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
template <typename T>
void f(T &t)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
g(t);
}
template <typename T>
void f(const T &t)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
g(t);
}
int main()
{
std::cout << "int &: ----------" << std::endl;
int x = 1;
f(x);
g(x);
std::cout << "const int &: ----" << std::endl;
const int y = 2;
f(y);
g(y);
std::cout << "int &&: ---------" << std::endl;
f(1);
g(1);
}
Od C++11, żeby rozwiązać podproblem #1, typ parametru powinien być zadeklarowany jako r-referencja bez kwalifikatorów.
Prawda objawiona:
template<typename T>
void
f(T &&t)
{
g(std::forward<T>(t));
}
Jeżeli T jest parametrem szablonu, to parametr funkcji typu T &&
nazywamy referencją przekazującą (ang. forwarding reference).
Mimo, że deklarowanym typem jest r-referencja, to na etapie kompilacji
typ r-referencji może zostać przekształcony do typu l-referencji.
Prawda objawiona, bo dla referencji przekazującej wprowadzono w C++11
specjalne zasady wnioskowania typu T w zależności od kategorii
argumentu, co jest dalej wyjaśnione.
Problem w tym, że parametr t jest l-wartością (bo ma nazwę t),
nawet jeżeli argumentem wywołania funkcji f była r-wartość. W ten
sposób tracimy informację o kategorii wartości wyrażenia, które było
argumentem wywołania funkcji f. Funkcja std::forward odzyskuje tę
kategorię wartości, czego szczegóły są wyjaśnione niżej.
Podproblem #1 został rozwiązany referencją przekazującą.
Podproblem #2 został rozwiązany funkcją std::forward.
Jaki będzie wywnioskowany argument dla parametru T szablonu, jeżeli
jest on użyty w deklaracji referencji przekazującej?
Jeżeli argumentem funkcji f jest:
l-wartość typu A, to T = A &,
r-wartość typu A, to T = A.
Wnioskowanie argumentów szablonu jest stosowane nie tylko przy
wywołaniu szablonów funkcji, ale także przy wywołaniu konstruktora
typu szablonowego. Konstruktor dla typu szablonowego może mieć
parametr T &&, gdzie T jest parametrem szablonu, dla którego mogą
być użyte zasady wnioskowania dla referencji przekazującej.
Ale jest tu pewien niuans, którego nie potrafię uzasadnić
([temp.deduct.call#3]). Parametr konstruktora typu T &&:
jest referencją przekazującą, jeżeli T jest parametrem szablonu
konstruktora,
nie jest referencją przekazującą, jeżeli T jest parametrem
szablonu typu.
Oto przykład:
#include <iostream>
struct A
{
// Here t is a forwarding reference.
template <typename T>
A(T &&t)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
};
template <typename T>
struct B
{
// Here t is not a forwarding reference, just an rvalue reference.
B(T &&t)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
};
int
main()
{
int x;
// Works the way we would expect from a forwarding reference.
A a1(1);
A a2(x);
B b1(1);
// An rvalue reference cannot bind to an lvalue.
// B b2(x);
}
W C++ nie ma typu referencji do referencji, ale takie typy mogą się
pojawić, jako efekt deklaracji referencji przekazującej, albo
definicji typów szablonowych z użyciem using czy typedef.
Jeżeli argumentem parametru szablonu T będzie A &, to wtedy typem
parametru funkcji, który został zadeklarowana jako referencja
przekazująca, będzie typ A & &&. Co wtedy?
Jeżeli pojawi się typ referencji do referencji, to kompilator zamieni taki typ na referencję według zasady:
cv1 A & cv2 & -> cv1 A &cv1 A & cv2 && -> cv1 A &cv1 A && cv2 & -> cv1 A &cv1 A && cv2 && -> cv1 A &&Zbiory cv1 i cv2 oznaczają zbiory kwalifikatorów, do których mogą
należeć const i volatile. Zbiór cv2, który określałby
kwalifikatory zagnieżdżonego typu referencyjnego (tego z lewej
strony), jest pomijany, ponieważ typy referencyjne nie mają
kwalifikatorów.
Spośród powyższych czterech przypadków, dla referencji przekazującej
może wystąpić jedynie kombinacja & &&, kiedy argumentem funkcji jest
l-wartość. Wtedy referencja przekazująca jest spłaszczana do
l-referencji, żeby można ją było zainicjalizować l-wartością.
Wyczerpujący test spłaszczania referencji z pominięciem volatile:
#include <iostream>
#include <type_traits>
using namespace std;
// A reference does not have a top-level qualifier.
int
main()
{
using l_type = int &;
using cl_type = const int &;
using r_type = int &&;
using cr_type = const int &&;
// A non-const lvalue reference to all other reference types.
// int & & -> int &
static_assert(is_same_v<l_type &, int &>);
// const int & & -> const int &
static_assert(is_same_v<cl_type &, const int &>);
// int && & -> int &
static_assert(is_same_v<r_type &, int &>);
// const int && & -> const int &
static_assert(is_same_v<cr_type &, const int &>);
// A const lvalue reference to all other reference types.
// int & const & -> int &
static_assert(is_same_v<l_type const &, int &>);
// const int & const & -> const int &
static_assert(is_same_v<cl_type const &, const int &>);
// int && const & -> int &
static_assert(is_same_v<r_type const &, int &>);
// const int && const & -> const int &
static_assert(is_same_v<cr_type const &, const int &>);
// A non-const rvalue reference to all other reference types.
// int & && -> int &
static_assert(is_same_v<l_type &&, int &>);
// const int & && -> const int &
static_assert(is_same_v<cl_type &&, const int &>);
// int && && -> int &&
static_assert(is_same_v<r_type &&, int &&>);
// const int && && -> const int &&
static_assert(is_same_v<cr_type &&, const int &&>);
// A const rvalue reference to all other reference types.
// int & const && -> int &
static_assert(is_same_v<l_type const &&, int &>);
// const int & const && -> const int &
static_assert(is_same_v<cl_type const &&, const int &>);
// int && const && -> int &&
static_assert(is_same_v<r_type const &&, int &&>);
// const int && const && -> const int &&
static_assert(is_same_v<cr_type const &&, const int &&>);
}
std::forwardFunkcja szablonowa std::forward przyjmuje l-wartość t typu T i w
zależności od argumentu szablonu zwraca:
std::forward<T>(t)std::forward<T &>(t)Funkcji std::forward używamy w definicji funkcji szablonowej, kiedy
trzeba odzyskać kategorię argumentu wywołania funkcji.
Przykład:
#include <iostream>
#include <utility>
using namespace std;
void
foo(int &)
{
cout << "foo dla l-wartości\n";
}
void
foo(int &&)
{
cout << "foo dla r-wartości\n";
}
int
main()
{
int x = 1;
foo(forward<int &>(x));
foo(forward<int>(x));
}
Funkcja może przyjmować przez referencję przekazującą dowolną liczbę
argumentów, które doskonale przekażemy, jak w przykładzie niżej. To
jest też jedna z motywacji wprowadzenia szablonu wariadycznego:
implementacja funkcji std::make_unique.
#include <memory>
#include <utility>
#include <vector>
using namespace std;
template <typename T, typename ... P>
auto
my_make_unique(P &&...p)
{
return unique_ptr<T>(new T{std::forward<P>(p)...});
}
int
main()
{
auto p = my_make_unique<std::vector<int>>(1, 2, 3);
}
A tu skrócony zapis z użyciem specyfikatora auto:
#include <memory>
#include <utility>
#include <vector>
using namespace std;
template <typename T>
auto
my_make_unique(auto &&...p)
{
return unique_ptr<T>(new T{std::forward<decltype(p)>(p)...});
}
int
main()
{
auto p = my_make_unique<std::vector<int>>(1, 2, 3);
}
Referencja przekazująca może być zwykłą zmienną lub polem składowym.
Typ auto && to referencja przekazująca, a nie r-referencja, ponieważ
wynikowym typem może być dowolny typ referencyjny: l-referencja,
referencja stała albo r-referencja.
int
main()
{
int x = 1;
const int y = 2;
// This becomes an lvalue reference, because auto = int &
auto &&lr = x;
// This becomes a const reference, because auto = const int &
auto &&cr = y;
// This becomes an rvalue reference, because auto = int
auto &&rr = 1;
}
Z referencji przekazującej możemy skorzystać, kiedy chcemy zainicjalizować referencję do elementu zwracanego przez funkcję, ale nie znamy zarówno typu jak i kategorii zwracanej wartości:
#include <set>
#include <vector>
int
main()
{
std::vector<int> vi = {1};
std::set<int> s = {1};
// We can initialize a non-const reference to a vector element,
// because the dereference operator returns a non-const lvalue
// reference to a vector element.
int &a = *vi.begin();
// We cannot initialize a non-const reference to a set element.
// int &b = *s.begin();
// The reference has to be const, because the dereference operator
// returns a const lvalue reference to a set element.
const int &c = *s.begin();
// It's best to let the compiler figure out the right type. As part
// of the type deduction, e becomes a const reference.
auto &d = *vi.begin();
auto &e = *s.begin();
std::vector<bool> vb = {true};
// We cannot initialize an lvalue reference, because the
// initializing expression is an rvalue: the dereference operator
// returns a temporary proxy object that can convert to a bool.
// auto &f = *vb.begin();
// We have to use an rvalue reference.
bool &&f = *vb.begin();
// So it's best to use the forwarding reference:
auto &&g = *vi.begin();
auto &&h = *s.begin();
auto &&i = *vb.begin();
}
#include <iostream>
#include <utility>
template <typename T>
struct A
{
T &&m_ref;
A(T &&ref): m_ref(std::forward<T>(ref))
{
}
T &&
operator*()
{
return std::forward<T>(m_ref);
}
};
// A deduction guide.
template<typename T>
A(T &&) -> A<T>;
void foo(int &i)
{
std::cout << "lvalue: " << i << std::endl;
}
void foo(int &&i)
{
std::cout << "rvalue: " << i << std::endl;
}
int
main()
{
int x = 10;
foo(*A<int &>(x));
foo(*A<int>(1));
// To have the arguments deduced as for a forwarding reference, we
// had to define a deduction guide.
foo(*A(x));
foo(*A(2));
}
Zdefiniujemy różne przeciążenia dla funkcji g. Funkcja g będzie
przyjmowała też drugi parametr, który pozwoli nam stwierdzić, czy
kompilator wybrał to przeciążenie, które się spodziewaliśmy. W
funkcji main wywołujemy każde przeciążenie funkcji.
Piszemy funkcję f, która doskonale przekazuje swój argument do
funkcji g.
Przykład:
#include <iostream>
#include <utility>
using namespace std;
void
test(const string &s1, const string &s2)
{
if (s1 != s2)
cout << "Expected: " << s1 << ", got: " << s2 << endl;
}
void
g(int &, const string &s)
{
cout << __PRETTY_FUNCTION__ << endl;
test(s, "int &");
}
void
g(const int &, const string &s)
{
cout << __PRETTY_FUNCTION__ << endl;
test(s, "const int &");
}
void
g(int &&, const string &s)
{
cout << __PRETTY_FUNCTION__ << endl;
test(s, "int &&");
}
template<typename T>
void
f(T &&t, const string &s)
{
cout << __PRETTY_FUNCTION__ << endl;
// g(t, s);
g(forward<T>(t), s);
}
int main()
{
// Test "int &"
int x;
g(x, "int &");
f(x, "int &");
// Test "const int &"
const int &y = 1;
g(y, "const int &");
f(y, "const int &");
// Test "int &&"
g(1, "int &&");
f(1, "int &&");
// We pass an rvalue of double type, but expect the "int &&"
// overload of g to be called. Inside function f, when passing
// parameter t to function g, type conversion from double to int
// takes place, creating a temporary. That conversion is allowed
// and required there, just like in C.
g(1.0, "int &&");
f(1.0, "int &&");
}
Tutaj ten sam przykład, ale sprawdzający argument funkcji (ten drugi)
zamieniliśmy na sprawdzający argument szablonu. Teraz sprawdzamy typ
w czasie kompilacji z użyciem static_assert:
#include <iostream>
#include <utility>
using namespace std;
template <typename T>
void
g(int &)
{
cout << __PRETTY_FUNCTION__ << endl;
static_assert(std::is_same_v<T, int &>);
}
template <typename T>
void
g(const int &)
{
cout << __PRETTY_FUNCTION__ << endl;
static_assert(std::is_same_v<T, const int &>);
}
template <typename T>
void
g(int &&)
{
cout << __PRETTY_FUNCTION__ << endl;
static_assert(std::is_same_v<T, int &&>);
}
template<typename C, typename T>
void
f(T &&t)
{
cout << __PRETTY_FUNCTION__ << endl;
// g<C>(t);
g<C>(forward<T>(t));
}
int main()
{
// Test "int &"
int x;
g<int &>(x);
f<int &>(x);
// Test "const int &"
const int &y = 1;
g<const int &>(y);
f<const int &>(y);
// Test "int &&"
g<int &&>(1);
f<int &&>(1);
// We pass an rvalue of double type, but expect the "int &&"
// overload of g to be called. Inside function f, when passing
// parameter t to function g, type conversion from double to int
// takes place, creating a temporary. That conversion is allowed
// and required there, just like in C.
g<int &&>(1.0);
f<int &&>(1.0);
}
Co się stanie, jeżeli usuniemy funkcję forward z funkcji f? Wtedy
będą przekazywane zawsze l-wartości do funkcji g. Można sprawdzić.
Problem doskonałego przekazywania argumentów występuje w programowaniu uogólnionym z użyciem szablonów.
Chodzi o przekazanie argumentów przez referencję (aby uniknąć kopiowania bądź przenoszenia) i wywołanie właściwego przeciążenia funkcji.
Żeby doskonale przekazać argument wywołania funkcji, używamy
referencji przekazującej i funkcji std::forward.
Jakie są dwa podproblemy doskonałego przekazywania argumentów?
Do czego służy funkcja std::forward?
Co to jest referencja przekazująca?