cpp

Wprowadzenie

Chielibyśmy, żeby wyrażenia wywołania mogły użyć:

Czas kompilacji czy uruchomienia

Callable może być określone (przez podanie referencji, wskaźnika czy obiektu) w czasie uruchomienia, albo kompilacji.

Callable w czasie uruchomienia

Jeżeli callable jest ustalane w czasie uruchomienia, to kod callable będzie wywołany i nie będzie wkompilowany (ang. inlined) w miejsce wywołania, no bo nie wiadomo co wkompilować. Proszę sprawdzić kod wynikowy przykładu niżej (z Code Explorer lub z użyciem narzędzia objdump).

int
inc(int i)
{
  return ++i;
}

int
dec(int i)
{
  return --i;
}

int
main()
{
  // Will it be true or false?
  volatile bool flag;

  // The callable is a pointer to a function.
  auto f = flag ? dec : inc;
  return f(1);
}

Callable w czasie kompilacji

Jeżeli w czasie kompilacji callable jest znane i nie zmienia się w czasie uruchomienia, to może ono być wkompilowane w miejscu wywołania. Jeżeli chcemy użyć inne callable, to musimy zmienić kod źródłowy. Oto przykład:

int
foo(int i)
{
  return ++i;
}

int
main()
{
  // The callable is a pointer to a function.
  auto f = foo;
  return f(1);
}

Przykład niżej pokazuje wkompilowanie domknięcia. Przykład pokazuje też brak narzutu wydajnościowego użycia domknięcia, co można sprawdzić z użyciem Code Explorer lub narzędziem objdump.

int
main()
{
  // The callable is a closure.
  auto f = [](int i){return ++i;};
  volatile int i = 1;
  return f(i);
}

Typ przyjmowanego callable

Typ przyjmowanego (przez funkcję czy konstruktor) callable określamy jako typ parametru funkcji czy konstruktora. Ten typ możemy zdefiniować na trzy sposoby: dokładnie, z użyciem std::function, lub szablonowo.

Dokładnie zdefiniowany typ callable

Możemy dokładnie określić typ przyjmowanego callable jako typ wskaźnika lub referencji na funkcję, jak w przykładzie niżej. Co ciekawe, wyrażenie z domknięciem (w przykładzie jest to []{cout << "World!\n";}) może być niejawnie rzutowane na wskaźnik na funkcję domknięcia, dlatego ten przykład się kompiluje.

#include <iostream>

using namespace std;

using callable = void();

// Here we accept a pointer to a callable.
void
f1(callable *c)
{
  c();
}

// The parameter type is the same as above.  It's a pointer to a
// function.  We could say that the function type decays into the
// function pointer type, because a function cannot be passed by
// value.

// void
// f1(callable c)
// {
//   c();
// }

void
f2(callable &c)
{
  c();
}

// This function is a callable.
void
g()
{
  cout << "Hello ";
}

// This is a functor struct.
struct callme
{
  void
  operator()()
  {
  }
};

int
main()
{
  // Here we pass a regular pointer to a function.
  f1(g);
  f2(g);

  // Here we implicitly get a pointer to the closure function.  The
  // closure decays into a pointer type.
  f1([]{cout << "World!\n";});
  // We cannot implicitly get a pointer to a closure function if the
  // lambda captured some data (these would be stored as member
  // fields, and we would need a pointer to the closure).
  int x;
  //  f1([x]{cout << "World!\n";});

  // Doesn't work for the function reference type.
  // f2([]{cout << "World!\n";});

  // This would not compile, because a functor does not convert
  // (implicitly or explicitly) to a function pointer.  A functor is
  // an object, which we default-construct: callme().
  //
  // f1(callme());
  // f2(callme());
}

Możemy też określić typ przyjmowanego callable jako konkretny typ funktora.

#include <iostream>

using namespace std;

// This is a functor struct.
struct callme
{
  void
  operator()()
  {
    cout << "Hello World!\n";
  }
};

void
f(callme c)
{
  c();
}

// This function is a callable.
void
g()
{
  cout << "Hello ";
}

int
main()
{
  // We pass a functor.
  f(callme());
 
  // We can't pass a function pointer.
  //
  // f(g);

  // We can't pass a closure.
  //
  // f([]{cout << "World!\n";});
}

std::function jako callable

Klasa szablonowa std::function dale możliwość przekazywania dowolnego callable: wskaźnika, funktora czy domknięcia, pod warunkiem, że callable ma wymagane typy parametrów i wyniku, które podajemy jako argument szablonu.

Dwie ważne funkcjonalności std::function:

Ta funkcjonalność std::function niestety jest okupiona narzutem wydajnościowym pośredniego wywołania i kopiowania callable. Jeżeli tej funkcjonalności nie potrzebujemy, to nie powinniśmy używać std::function.

#include <iostream>
#include <functional>

using namespace std;

using callable = void();

void
f(std::function<callable> c)
{
  c();
}

// This is a functor struct.
struct callme
{
  callme() = default;

  callme(const callme &)
  {
    cout << "copy-ctor\n";
  }

  void
  operator()()
  {
    cout << "Hello";
  }
};

// This function is a callable.
void
g()
{
  cout << " World";
}

int
main()
{
  // We pass a functor.
  f(callme());
 
  // We pass a function pointer.
  f(g);

  // We pass a closure.
  f([]{cout << "!\n";});

  function<callable> c = callme();
  f(c);
  c = g;
  f(c);
  c = []{cout << "!\n";};
  f(c);    
}

Szablonowy typ callable

Typ callable może być parametrem szablonu. Wtedy na etapie kompilacji funkcja jest konkretyzowana dla tego konkretnego callable, co przekłada się na maksymalną wydajnośc, bo kod wynikowy generowany jest “na miarę”.

Użycie szablonu nie wyklucza użycia std::function, która może być po prostu argumentem szablonu.

#include <iostream>
#include <functional>

using namespace std;

using callable = void();

template <typename C>
void
f(C c)
{
  c();
}

// This is a functor struct.
struct callme
{
  callme() = default;

  callme(const callme &)
  {
    cout << "copy-ctor\n";
  }

  void
  operator()()
  {
    cout << "Hello";
  }
};

// This function is a callable.
void
g()
{
  cout << " World";
}

int
main()
{
  // We pass a functor.
  f(callme());
 
  // We pass a function pointer.
  f(g);

  // We pass a closure.
  f([]{cout << "!\n";});

  // We can pass an std::function too.
  f(std::function<callable>(callme()));
  f<std::function<callable>>(callme());
}

Funkcja składowa klasy

Funkcja składowa klasy też jest callable, ale do jej wywołania potrzebujemy dodatkowo obiektu, na rzecz którego funkcja będzie wywołana. Funkcję składową f możemy wywołać przez nazwę tak: o.f(lista argumentów), gdzie o jest nazwą obiektu, albo p->f(lista argumentów), gdzie p jest wskaźnikiem na obiekt.

Funkcję składową możemy przekazać, ale tylko przez wskaźnik, bo nie ma referencji na funkcję składową, chociaż referencja do zwykłej funkcji jest. Typ wskaźnika na funkcję składową jest podobny do typu wskaźnika na funkcję, ale deklarator * poprzedzamy zakresem klasy, np. A::. Adres funkcji składowej pobieramy operatorem adresowania, np. &A::foo, bo nazwa funkcji składowej nie rozpadnie się (ang. decay) na wskaźnik do niej (takiego rozpadu nie ma).

Składnia wywołania funkcji składowej przez wskaźnik nieco się różni od składni wywołania funkcji. Składnia jest taka: (o.*p)(lista argumentów), gdzie o jest obiektem, a p wskaźnikiem na funkcję składową. Ważne są nawiasy wokół o.*p i miejsce operatora wyłuskania.

Przykład niżej pokazuje także użycie std::function razem ze wskaźnikiem na funkcję składową. Musimy użyć funkcji std::bind, żeby przekazać obiekt, na rzecz którego funkcja będzie wywoływana.

#include <iostream>
#include <functional>
#include <string>

using namespace std;

struct A
{
  string m_name;

  A(string name): m_name(std::move(name))
  {
  }

  void foo()
  {
    cout << __PRETTY_FUNCTION__ << " for " << m_name << endl;
  }

  void goo()
  {
    cout << __PRETTY_FUNCTION__ << " for " << m_name << endl;
  }
};

int
main()
{
  A a("a"), b("b");

  // A pointer to any member function of A with the defined interface.
  void (A::* p1)() = &A::foo;
  // It's better to let the compiler deduce the type.
  auto p2 = &A::goo;
  // Make sure the types are the same.
  static_assert(std::is_same_v<decltype(p1), decltype(p2)>);

  (a.*p1)();
  (b.*p1)();
  (a.*p2)();
  (b.*p2)();

  // We can also store a pointer to a member function with an object
  // to call.
  std::function<void ()> f = std::bind(p1, a);
  f();
  f = std::bind(p2, b);
  f();
}

Tę składnię łatwo pomylić ze składnią wywołania funkcji, do której wskaźnik jest przechowywany jako pole składowe obiektu. Oto przykład:

#include <iostream>

void
foo()
{
  std::cout << __PRETTY_FUNCTION__ << std::endl;
}

struct A
{
  void (*p)();
};

int
main()
{
  A a;
  a.p = foo;
  (*a.p)();
  a.p();
}

Użycie wkaźnika na funkcję składową jest zaawansowaną funkcjonalnością, którą oczywiście stosuje się wyłącznie w programowaniu obiektowym, ale którą także uwzględniono w programowaniu uogólnionym, w funkcji std::invoke.

std::invoke

Funkcja std::invoke wywołuje callable z użyciem podanych argumentów. Wszystkie argumenty std::invoke są doskonale przekazywane. Na pierwszy rzut oka ta funkcja robi to samo, co zwykłe wywołanie callable. Oto przykład:

#include <functional>
#include <iostream>

using namespace std;

int
foo(int x)
{
  cout << "foo: ";
  return ++x;
}

int
main()
{
  cout << foo(1) << endl;
  cout << invoke(foo, 1) << endl;

  auto lambda = [](int x){cout << "lambda: "; return ++x;};
  cout << lambda(1) << endl;
  cout << invoke(lambda, 1) << endl;
}

Dlaczego więc wprowadzono tę funkcję? Bo pozwala także na wywołanie składowej klasy. Funkcja wprowadza ujednolicony sposób wywołania dowolnego callable, co jest potrzebne w programowaniu uogólnionym.

Argumentami std::invoke przy wywołaniu funkcji składowej kolejno są:

Oto przykład:

#include <functional>
#include <iostream>
#include <string>

using namespace std;

struct A
{
  string m_name;

  A(const string &name): m_name(name)
  {
  }

  void foo(int x)
  {
    cout << __PRETTY_FUNCTION__ << " for " << m_name << endl;
  }

  void goo(int x)
  {
    cout << __PRETTY_FUNCTION__ << " for " << m_name << endl;
  }
};

int
main()
{
  A a("a"), b("b");  
  invoke(&A::foo, a, 1);
  invoke(&A::foo, b, 2);
  invoke(&A::goo, a, 3);
  invoke(&A::goo, b, 4);
}

W przykładzie niżej, funkcja f wywołuje otrzymany callable, którym może być także wskaźnik na funkcję składową. Zaimplementowanie takiej funkcjonalności samemu byłoby dosyć trudne, a my to osiągnęliśmy jedynie korzystając z funkcji std::invoke.

#include <iostream>
#include <functional>
#include <string>

using namespace std;

template <typename G, typename ... Args>
decltype(auto)
f(G &&g, Args &&... args)
{
  cout << __PRETTY_FUNCTION__ << '\n';
  return std::invoke(std::forward<G>(g), std::forward<Args>(args)...);
}

void
foo()
{
  cout << __PRETTY_FUNCTION__ << '\n';
}

template <typename T>
void
goo(T)
{
  cout << __PRETTY_FUNCTION__ << '\n';
}

struct A
{
  void foo()
  {
    cout << __PRETTY_FUNCTION__ << '\n';
  }

  template <typename T>
  void goo(T)
  {
    cout << __PRETTY_FUNCTION__ << '\n';
  }
};

int
main()
{
  A a;

  f(foo);
  f(goo<int>, 1);
  f(&A::foo, a);
  f(&A::goo<int>, A(), 1);
}

W przykładzie wyżej użyliśmy szablonu wariadycznego, żeby funkcja f mogła przyjąć dowolną (także zerową) liczbę argumentów dla wywoływanego callable. Są dwa problemy powyższego przykładu związane z użyciem std::invoke:

std::apply

Funkcja std::apply pozwala na wywołanie dowolnego callable:

Krotkę tworzymy z użyciem funkcji std::forward_as_tuple, żeby zachować kategorię wartości wyrażeń elementów krotki. Jeżeli wywołujemy funkcję składową, to pierwszym elementem krotki powinien być obiekt, na rzecz którego callable będzie wywołana.

#include <iostream>
#include <functional>
#include <string>
#include <tuple>

using namespace std;

template <typename G, typename T, typename E>
void
f(G &&g, T &&t, const E &e)
{
  cout << __PRETTY_FUNCTION__ << ": " << e << '\n';
  std::apply(std::forward<G>(g), std::forward<T>(t));
}

void
foo()
{
  cout << __PRETTY_FUNCTION__ << '\n';
}

template <typename T>
void
goo(T)
{
  cout << __PRETTY_FUNCTION__ << '\n';
}

struct A
{
  void foo()
  {
    cout << __PRETTY_FUNCTION__ << '\n';
  }

  template <typename T>
  void goo(T)
  {
    cout << __PRETTY_FUNCTION__ << '\n';
  }
};

int
main()
{
  A a;

  f(foo, make_tuple(), "Hello");
  f(goo<int>, make_tuple(1), 3.14159);
  f(&A::foo, forward_as_tuple(a), .1);
  f(&A::goo<int>, forward_as_tuple(A(), 1), 1);
}

Funkcje std::invoke, std::apply i std::forward_as_tuple są mechanizmami czasu kompilacji i nie wprowadzają żadnego narzutu czasowego i pamięciowego.

Przeciążenia

Niestety nie udało mi się użyć funkcji std::invoke i std::apply z przeciążeniami:

#include <functional>
#include <iostream>

// Regular functions -------------------------------------------------
void foo(int &x)
{
  std::cout << "Lvalue overload\n";
}

void foo(int &&x)
{
  std::cout << "Rvalue overload\n";
}

void goo()
{
}

// A template function -----------------------------------------------
template <typename T>
void goo(T)
{
}

struct A
{
  // Member functions ------------------------------------------------
  void foo() &
  {
  }

  void foo() &&
  {
  }
};

int
main()
{
  int x;
  A a;

  // Regular function call -------------------------------------------
  foo(x);
  foo(1);

  // We don't want to use static_cast, it's not generic.
  std::invoke(static_cast<void(*)(int &)>(foo), x);
  std::invoke(static_cast<void(*)(int &&)>(foo), 1);
  // std::invoke(foo, x);
  // std::invoke(foo, 1);

  // std::apply(foo, std::forward_as_tuple(x));
  // std::apply(foo, std::forward_as_tuple(1));

  // Template function call ------------------------------------------
  goo();
  goo(1);

  // std::invoke(goo);
  // std::invoke(goo, 1);

  // std::apply(goo, std::make_tuple());
  // std::apply(goo, std::forward_as_tuple(1));

  // Member function call --------------------------------------------
  a.foo();
  A().foo();

  // std::invoke(&A::foo, a);
  // std::invoke(&A::foo, A());

  // std::apply(&A::foo, std::forward_as_tuple(a));
  // std::apply(&A::foo, std::forward_as_tuple(A()));
}

Doskonałe przekazywanie

Callable powinniśmy doskonałe przekazywać (może lepiej: doskonale wywoływać). Funkcji (f w przykładzie niżej) przekazujemy callable jako argument, a funkcja powinna zachować kategorię wartości argumentu przy wywoływaniu otrzymanego callable. Zachowanie kategorii nie ma znaczenia, kiedy przekazywana jest funkcja (bo funkcja zawsze jest l-wartością), ale ma znaczenie, kiedy przekazujemy funktor albo domknięcie, bo wtedy argument może być l-wartością albo r-wartością.

#include <iostream>
#include <utility>

using namespace std;

template <typename C>
void f(C &&c)
{
  cout << __PRETTY_FUNCTION__ << '\n';
  // Here we perfectly forward (or rather call) the callable.
  std::forward<C>(c)();
}

void g()
{
}

struct callme
{
  void operator()() &
  {
    cout << "lvalue of callme\n";
  }

  void operator()() &&
  {
    cout << "rvalue of callme\n";
  }
};

int main()
{
  // Because function f accepts a callable by a forwarding reference,
  // we can pass to it by reference both a function and a closure.

  // Expression g is an lvalue.
  f(g); // C = void (&)()
  // The lambda expression in an rvalue.
  f([]{});

  // Forwarding the callable in function f is needed here.
  callme c;
  // Function f should call the passed callable as an lvalue.
  f(c);
  // Function f should call the passed callable as an rvalue.
  f(callme());
}

Podsumowanie

Quiz