Chielibyśmy, żeby wyrażenia wywołania mogły użyć:
dowolnego callable w czasie kompilacji bez utraty wydajności,
dowolnego callable w czasie uruchomienia, niestety już ze stratą wydajności,
także funkcji składowej jako callable.
Callable może być określone (przez podanie referencji, wskaźnika czy obiektu) w czasie uruchomienia, albo kompilacji.
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);
}
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 (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.
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 callableKlasa 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
:
Obiekt klasy std::function
opakowuje i przechowuje przez wartość
przekazany callable (więc callable jest kopiowany), udostępniając
ujednolicony interfejs (jeden składowy operator wywołania)
niezależny od typu przechowywanego callable.
Obiekt klasy std::function
może mieć zmieniany callable w czasie
uruchomienia; te callable mogą mieć różne typy, ale muszą mieć ten
sam interfejs.
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);
}
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 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ą:
wskaźnik na funkcję składową,
obiekt na rzecz którego wywoływana jest funkcja składowa,
argumenty dla funkcji składowej.
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
:
konieczność użycia szablonu wariadycznego,
po argumentach dla callable nie można przekazać innych argumentów, bo wszysto do końca jest traktowane jako argumenty dla callable.
std::apply
Funkcja std::apply
pozwala na wywołanie dowolnego callable:
bez użycia szablonu wariadycznego,
gdzie argumenty są przekazywane w jednej krotce.
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.
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()));
}
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());
}
Callable może być określane w czasie kompilacji albo uruchomienia.
Do wywołania otrzymanego callable najlepiej jest użyć std::invoke
.
Typ std::function
jest narzędziem czasu uruchomienia.
Szablony funkcji std::invoke
i std::apply
są narzędziami czasu
kompilacji.
Do czego służy std::function
?
Jaka jest różnica między zwykłym wywołaniem funkcji, a wywołaniem z
użyciem funkcji std::invoke
?
Jaka jest różnica między funkcjami std::invoke
and std::apply
?