cpp

Wprowadzenie

Problemem do rozwiązania jest napisanie funkcji f, która wywołuje funkcję g i doskonale przekazuje jej swój argument. Doskonale, czyli:

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):

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.

Dlaczego to problem?

Zadanie jest problematyczne, bo argumentem wywołania funkcji może być albo l-wartość, albo r-wartość. Są dwa podproblemy.

Podproblem #1

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.

Podproblem #2

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.

Motywacja: fabryki obiektów

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));
}

Parametry i argumenty funkcji

Argument 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

Możliwe rozwiązania z pominięciem kwalifikatora volatile.

Nie bierzemy pod uwagę rozwiązań:

Rozwiązanie: T

Wyglą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:

Rozwiązanie: 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());
}

Rozwiązanie: 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);
}

Rozwiązanie: 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);
}

Prawidłowe rozwiązanie: T &&

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.

Wnioskowanie dla referencji przekazującej

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:

Konstruktor a referencja przekazująca

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 &&:

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);
}

Referencja do referencji

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?

Spłaszczanie typów referencyjnych

Jeżeli pojawi się typ referencji do referencji, to kompilator zamieni taki typ na referencję według zasady:

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 &&>);
}

Funkcja std::forward

Funkcja szablonowa std::forward przyjmuje l-wartość t typu T i w zależności od argumentu szablonu zwraca:

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));
}

Przypadek wariadyczny

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

Referencja przekazująca może być zwykłą zmienną lub polem składowym.

Zwykła zmienna

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();
}

Pole składowe

#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));
}

Rozbudowany przykład

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ć.

Podsumowanie

Quiz