C++模板 - 17(可调用性 Callables)
对于任何一门程序设计语言来说,函数都是必不可少的,它通常封装了某个功能,由指定的输入得到某个输出。另外,它还有一个显著的语言特征,就是它的可调用性(Callables)。在C++模板的应用中,这个可调用性本身可以作为一个模板参数类型(特别是泛型库的作者,会大量依赖于可调用性,比如说STL中的算法)。
template<typename F>
void log(F f) {
std::cout << "log called" << std::endl;
f();
}
对于模板参数F的唯一要求就是能够调用它,也就是对于它的对象可以使用()调用符。
那么C++中的哪些东西符合这个要求呢?
- 函数指针类型
- 重载了运算符()的类,包括lambda表达式(这个称为functor)
- 定义了转换为某个函数指针或引用的运算符的类(这个称为function object)
下面我们一个一个看看
void show_me() {
std::cout << "I am a function" << std::endl;
}
log(show_me); // decay to pointer, log(&show_me) maybe clearer
class foo {
public:
void operator()() const { std::cout << "I am a functor" << std::endl; }
};
log(foo{});
log([]() { std::cout << "I am a lambda" << std::endl; });
class bar {
public:
operator decltype(show_me) * () const { return bar::show_bar; }
static void show_bar() {
std::cout << "I am a function object" << std::endl;
;
}
};
log(bar{});
你看到这里,也许心中会有一丝丝的疑问:上面的例子是不是漏掉了一种同样具备可调用性的东东呢?比如说类的成员函数?是的,类的成员函数也可以使用()来调用,但是它不满足函数模板log对模板参数类型的要求,因为类的成员函数的调用形式为object.mem_func()或obj_ptr->mem_func(),而log要求的调用形式为func()。
那能不能让log也同时支持类的成员函数呢?可以,STL提供了std::invoke,可以非常方便对所有可调用性的对象进行调用包装。下面我们用另外一种方式来实现log函数模板,可以更清楚地看到普通函数调用和类成员函数调用之间的区别。
#include <type_traits>
template<typename T, typename U>
void log(void (T::*f)(), U&& caller) {
if constexpr (std::is_member_function_pointer_v<decltype(f)>) {
if constexpr (std::is_base_of_v<T, std::decay_t<U>>) { // object
return (std::forward<U>(caller).*f)();
} else { // pointer
return ((*std::forward<U>(caller)).*f)();
}
}
};
这里我们增加一个log函数模板的重载定义,专门用来处理类成员函数这种情况。针对成员函数,我们通过检测调用对象来区别实例对象和指针调用这两种不同的调用形式。
class bar {
public:
operator decltype(show_me) * () const { return bar::show_bar; }
void show_mem_bar() { std::cout << "I am a member function" << std::endl; }
static void show_bar() { std::cout << "I am a function object" << std::endl; }
};
这次我们在bar类中添加了成员函数show_mem_bar的定义,用来测试上面这个log版本。
bar bar_obj;
log(&bar::show_mem_bar, bar_obj);
bar* bar_ptr = &bar_obj;
log(&bar::show_mem_bar, bar_ptr);
通过类的对象和指针,都成功地调用了成员函数。现在这个log就可以处理所有void()签名的函数调用了。当然如果你打算让这个log更加通用一些,可以处理任意签名的函数调用,也非常容易,只需要配合decltype(auto)和变长模板参数就可以了。