Callable concept/named requirement
Callable named requirement是一个比function object/function更加宽泛的概念。
C++对callable named requirement的实现、描述是符合named requirement的原则的:
1、它是behavior-based(在cppreference C++ named requirements: Callable中有所描述)
2、C++ standard library提供了:
- 操作callable的uniform interface/magic function
- trait
NOTE:
1 uniform interface在
./interface
章节中进行了描述2 关于 named requirement,参见
C++\Language-reference\Template\Programming-paradigm\Generic-programming\Named-requirements
章节
What's new in C++?
首先搞清楚C++提出callable concept/named requirement目的、它要解决的问题。由于它的uniform interface/magic function是std::invoke
,所以我们搞清楚std::invoke
所解决的问题和搞清楚callable concept/named requirement所要解决的问题是相同的:
在C++\Language-reference\Functions\Function
的"Function call expression"章节中,我们知道了C++中有多种类型都能够被用于function call expression,但是它的syntax并不统一,这就导致了generic programming需要由programmer处理多种情况: 需要由programmer进行abstraction和implementation,造成programmer的负担;std::invoke
就是提供了对所有这些callable的uniform invokation interface,显然这简化了generic programming。
bfilipek C++20 Ranges, Projections, std::invoke and if constexpr#std::invoke
, C++17
The primary motivation for this helper function is the issue with a non-uniform syntax for various callable objects.
For example, if you have a regular function object, you can just call:
func(args...)
But if you have a pointer to a member function then the syntax is different:
(obj.*funcPtr)(args...)
This might be an issue when you write a function template like:
template <typename T, typename F>
void CallOnRange(T& container, F f) {
for (auto&& elem : container)
f(elem);
}
std::vector v { 1, 2, 3, 4 };
CallOnRange(v, [](int i) { std::cout << i << '\n'; });
CallOnRange
works nicely for a regular function object type (like a lambda or a function pointer), but won’t work on pointers to member functions. In that case, we need to make additional overload:
template <typename TCont, typename Type, typename U>
void CallOnRange(TCont& container, Type U::* f)
{
for (auto&& elem : container)
(elem.*f)();
}
See the experiments at @Wandbox
That’s why, for those special cases, we can use std::invoke
which gives us uniform syntax call:
template <typename T, typename F>
void CallOnRangeInvoke(T& container, F f)
{
for (auto&& elem : container)
std::invoke(f, elem);
}
In short invoke(f, t1, t2, ..., tN)
(proposed in N4169 and accepted for C++17) can handle the following cases::
- function objects: like
func(arguments...)
- pointers to member functions
(obj.*funcPtr)(arguments...)
+ pointers and references - pointer to member data
obj.*pdata
+ pointers and references
As you can see, this expression creates a nice abstraction over several options that you can “call” something. No matter if that’s a pointer to a member function, a regular callable object, or even a data member.
How it works?
In principle, the function has to check the type of the callable object and then make a right call to and forward the arguments.
Fortunately, since C++17 all of those checks can be done with a relatively easy way! There’s no need for complicated SFINAE tricks, and in most of the cases the code can leverage if constexpr
.
To understand the code, we can look at the sample implementation @cppreference.
The main function std::invoke
wraps the call to the INVOKE
template function that has two overloads:
Here’s one for a regular function:
template <class F, class... Args>
constexpr decltype(auto) INVOKE(F&& f, Args&&... args) {
return std::forward<F>(f)(std::forward<Args>(args)...);
}
And then the overload for pointers to member functions or for data members:
template <class T, class Type, class T1, class... Args>
constexpr decltype(auto) INVOKE(Type T::* f, T1&& t1, Args&&... args) {
if constexpr (std::is_member_function_pointer_v<decltype(f)>) {
if constexpr (std::is_base_of_v<T, std::decay_t<T1>>)
return (std::forward<T1>(t1).*f)(std::forward<Args>(args)...);
else if constexpr (is_reference_wrapper_v<std::decay_t<T1>>)
return (t1.get().*f)(std::forward<Args>(args)...);
else
return ((*std::forward<T1>(t1)).*f)(std::forward<Args>(args)...);
} else {
static_assert(std::is_member_object_pointer_v<decltype(f)>);
static_assert(sizeof...(args) == 0);
if constexpr (std::is_base_of_v<T, std::decay_t<T1>>)
return std::forward<T1>(t1).*f;
else if constexpr (is_reference_wrapper_v<std::decay_t<T1>>)
return t1.get().*f;
else
return (*std::forward<T1>(t1)).*f;
}
}
One note: in C++17 std::invoke
wasn’t specified with constexpr
, it was added in C++20.
Thanks to if constexpr
(added in C++17) we can read this function in a “normal” way. As we can see the function checks
- if the callable is a
is_member_function_pointer
- this is a type trait available in the standard library, see here - otherwise we can assume that it’s a pointer to a non-static data member. For this case, there can be no arguments passed, only the
Here’s a simple code that demonstrates pointers to non static data members:
struct GameActor {
std::string name;
std::string desc;
};
int main(){
std::string GameActor::* pNameMember = &GameActor::name;
GameActor actor { "enemy", "super evil" };
std::cout << actor.name << " is " << actor.desc << '\n';
actor.*pNameMember = "friend";
pNameMember = &GameActor::desc;
actor.*pNameMember = "very friendly";
std::cout << actor.name << " is " << actor.desc << '\n';
}
See the code @Wandbox
If we look closer in the function implementation, you can also spot that std::invoke
then have three more cases:
- regular call - no dereferencing needed
- via reference wrapper - so we have to call
.get()
to get the object - in other cases we assume it’s a pointer and then we need to dereference it. This supports, for example, smart pointers.
struct GameActor {
std::string name;
std::string desc;
};
int main(){
GameActor actor { "robot", "a friendly type" };
std::cout << "actor is: " << std::invoke(&GameActor::name, actor) << '\n';
auto ptr = std::make_unique<GameActor>("space ship", "slow");
std::cout << "actor is: " << std::invoke(&GameActor::name, ptr) << '\n';
}
See code @Wandbox
We can also look at more sophisticated, production-ready implementation at MSVC/STL code here @Github. Surprisingly the code for invoke
is located in the type_traits
header and not in <functional>
.
(Bonus) Compilation Speed And Costs
If you wonder about the cost of extra compilation time and runtime performance of code with std::invoke
, you can see this early preview text available for Patrons:
Understanding std::invoke, CppCon 2020 Presentation notes, Compilation Speed of std::invoke
Or see all benefits at this summary site: Exlusive C++ Content.
open-std N4169
The aim of this proposal is to introduce the function template invoke
that provide uniform semantics for invoking all C++ callable types which includes:
1 function pointers,
2 member pointers and
3 functors.
The behaviour of the function is defined in terms of *INVOKE*
expression.
Implementability
Proposed invoke
function template may be implemented in terms of existing C++11 standard library components:
#include <type_traits> // std::enable_if
#include <functional> // std::mem_fn
template<typename Functor, typename ... Args>
typename std::enable_if<std::is_member_pointer<typename std::decay<Functor>::type>::value, typename std::result_of<Functor&& (Args&&...)>::type>::type invoke(Functor &&f, Args &&... args)
{
return std::mem_fn(f)(std::forward<Args>(args)...);
}
template<typename Functor, typename ... Args>
typename std::enable_if<!std::is_member_pointer<typename std::decay<Functor>::type>::value, typename std::result_of<Functor&& (Args&&...)>::type>::type invoke(Functor &&f, Args &&... args)
{
return std::forward<Functor>(f)(std::forward<Args>(args)...);
}
// g++ --std=c++11 test.cpp
An constexpr
implemenatation may be found at: https://github.com/tomaszkam/proposals/blob/master/invoke/invoke_cpp11.hpp.
NOTE: 上述链接已经失效,我找到正确的实现,fork到了我的GitHub中,下面是实现:
https://github.com/dengking/proposals/tree/master/implementation/invoke
cppreference C++ named requirements: Callable
A Callable type is a type for which the INVOKE operation (used by, e.g., std::function, std::bind, and std::thread::thread) is applicable. This operation may be performed explicitly using the library function std::invoke. (since C++17)
NOTE:
INVOKE operation 说明是 behavior-based。
关于std::invoke,参见
./Interface
章节
Requirements
The type T
satisfies Callable if
Given
f
, an object of typeT
ArgTypes
, suitable list of argument typesR
, suitable return type
The following expressions must be valid:
Expression | Requirements |
---|---|
INVOKE<R>(f, std::declval<ArgTypes>()...) |
the expression is well-formed in unevaluated context |
where INVOKE(f, t1, t2, ..., tN) is defined as follows:
NOTE: 从下面的描述可以看出,callable concept包含了C++中所有的可以进行invoke的情况,它是对它们的统一描述,显然这是uniform。
一、 if f
is a pointer to member function of class T
:
1、If std::is_base_of<T,
std::remove_reference_t<decltype(t1)>>::value
is true, then INVOKE(f, t1, t2, ..., tN)
is equivalent to (t1.*f)(t2, ..., tN)
2、otherwise, if std::remove_cvref_t<decltype(t1)>
is a specialization of std::reference_wrapper, then INVOKE(f, t1, t2, ..., tN)
is equivalent to (t1.get().*f)(t2, ..., tN)
(since C++17)
3、otherwise, if t1
does not satisfy the previous items, then INVOKE(f, t1, t2, ..., tN)
is equivalent to ((*t1).*f)(t2, ..., tN)
.
二、 otherwise, if N == 1 and f
is a pointer to data member of class T
:
1、If std::is_base_of<T,
std::remove_reference_t<decltype(t1)>>::value
is true, then INVOKE(f, t1)
is equivalent to t1.*f
2、otherwise, if std::remove_cvref_t<decltype(t1)>
is a specialization of std::reference_wrapper, then INVOKE(f, t1)
is equivalent to t1.get().*f
(since C++17)
3、otherwise, if t1
does not satisfy the previous items, then INVOKE(f, t1)
is equivalent to (*t1).*f
三、 otherwise, INVOKE(f, t1, t2, ..., tN)
is equivalent to f(t1, t2, ..., tN)
(that is, f
is a FunctionObject)
Notes
For pointers to member functions and pointers to data members, t1
may be a regular pointer or an object of class type that overloads operator*
, such as std::unique_ptr or std::shared_ptr.
Pointers to data members are Callable, even though no function calls take place.
Standard library
In addition, the following standard library facilities accept any Callable type (not just FunctionObject)
API |
---|
std::function |
std::bind |
std::result_of |
std::thread::thread |
std::call_once |
std::async |
std::packaged_task |
std::reference_wrapper |
NOTE: 可以认为,上述这些,在内部实现中,都使用了
std::invoke