目录
22.1 函数对象,指针,以及 std:function<>
在第 18 章中介绍了 C++中 static 多态(通过模板实现)和 dynamic 多态(通过继承和 virtual 函数实现)的本质。两种多态都给程序编写提供了功能强大的抽象,但是也都各有其不足: static 多态提供了和非多态代码一样的性能,但是其在运行期间所使用的类型在编译期就已 经决定。而通过继承实现的 dynamic 多态,则允许单一版本的多态函数适用于在编译期未知 的类型,但是该方式有点不太灵活,因为相关类型必须从统一的基类做继承。
在给模板提供定制化行为的时候,函数对象会比较有用。比如,下面的函数模板列举了从 0 到某个值之间的所有整数,并将每一个值都提供给了一个已有的函数对象 f:
- #include
- #include
- template<typename F>
- void forUpTo(int n, F f){
- for (int i = 0; i != n; ++i)
- {
- f(i); // call passed function f for i
- }
- }
- void printInt(int i)
- {
- std::cout << i << ’ ’;
- }
- int main()
- {
- std::vector<int> values;
- // insert values from 0 to 4:
- forUpTo(5,
- [&values](int i) {
- values.push_back(i);
- });
- // print elements:
- forUpTo(5, printInt); // prints 0 1 2 3 4
- std::cout << ’\n’;
- }
其中 forUpTo()函数模板适用于所有的函数对象,包括 lambda,函数指针,以及任意实现了 合适的 operator()运算符或者可以转换为一个函数指针或引用的类,
每一次对 forUpTo() 的使用都很可能产生一个不同的函数模板实例。上述例子中的函数模板非常小,但是如果该 模板非常大的话,这些不同应用导致的实例化很可能会导致代码量的增加。
一个缓解代码量增加的方式是将函数模板转变为非模板的形式,这样就不再需要实例化。比 如,我们可能会使用函数指针:
- void forUpTo(int n, void (*f)(int))
- {
- for (int i = 0; i != n; ++i)
- {
- f(i); // call passed function f for i
- }
- }
但是,虽然在给其传递 printInt()的时候该方式可以正常工作,给其传递 lambda 却会导致错 误:
标准库中的类模板 std::functional<>则可以用来实现另一种类型的 forUpTo():
- #include
- void forUpTo(int n, std::function<void(int)> f)
- {
- for (int i = 0; i != n; ++i)
- {
- f(i) // call passed function f for i
- }
- }
Std::functional<>的模板参数是一个函数类型,该类型体现了函数对象所接受的参数类型以及 其所需要产生的返回类型,非常类似于表征了参数和返回类型的函数指针。
这一形式的 forUpTo()提供了 static 多态的一部分特性:适用于一组任意数量的类型(包含函 数指针,lambda,以及任意实现了适当 operator()运算符的类),同时又是一个只有一种实 现的非模板函数。为了实现上述功能,它使用了一种称之为类型消除(type erasure)的技 术,该技术将 static 和 dynamic
Std::functional<>类型是一种高效的、广义形式的 C++函数指针,提供了与函数指针相同的基 本操作:
在调用者对函数本身一无所知的情况下,可以被用来调用该函数。
可以被拷贝,move 以及赋值。
可以被另一个(函数签名一致的)函数初始化或者赋值。
如果没有函数与之绑定,其状态是“null”。
但是,与 C++函数指针不同的是,std::functional<>还可以被用来存储 lambda,以及其它任意 实现了合适的 operator()的函数对象,所有这些情况对应的类型都可能不同。
会实现一版自己的广义函数指针类模板(FunctionPtr),会给 其提供相同的关键操作以及能力,并用之替换 std::functional:
- #include "functionptr.hpp"
- #include
- #include
- void forUpTo(int n, FunctionPtr<void(int)> f)
- {
- for (int i = 0; i != n; ++i)
- {
- f(i); // call passed function f for i
- }
- }
- void printInt(int i)
- {
- std::cout << i << ’ ’;
- }
- int main()
- {
- std::vector<int> values;
- // insert values from 0 to 4:
- forUpTo(5,[&values](int i) {
- values.push_back(i);
- });
- // print elements:
- forUpTo(5, printInt); // prints 0 1 2 3 4
- std::cout << ’\n’;
- }
FunctionPtr 的接口非常直观的提供了构造,拷贝,move,析构,初始化,以及从任意函数 对象进行赋值,还有就是要能够调用其底层的函数对象。
接口中最有意思的一部分是如何在 一个类模板的偏特化中对其进行完整的描述,该偏特化将模板参数(函数类型)分解为其组 成部分(返回类型以及参数类型):
- // primary template:
- template<typename Signature>
- class FunctionPtr;
- // partial specialization:
- template<typename R, typename… Args>
- class FunctionPtr<R(Args…)>
- {
- private:
- FunctorBridge
* bridge; - public:
- // constructors:
- FunctionPtr() : bridge(nullptr) {
- }
- FunctionPtr(FunctionPtr const& other); // see
- functionptrcpinv.hpp
- FunctionPtr(FunctionPtr& other)
- : FunctionPtr(static_cast
(other)) { - }
- FunctionPtr(FunctionPtr&& other) : bridge(other.bridge) {
- other.bridge = nullptr;
- }
- //construction from arbitrary function objects:
- template<typename F> FunctionPtr(F&& f); // see
- functionptrinit.hpp// assignment operators:
- FunctionPtr& operator=(FunctionPtr const& other) {
- FunctionPtr tmp(other);
- swap(*this, tmp);
- return *this;
- }
- FunctionPtr& operator=(FunctionPtr&& other) {
-
- delete bridge;
- bridge = other.bridge;
- other.bridge = nullptr;
- return *this;
- }
- //construction and assignment from arbitrary function objects:
- template<typename F> FunctionPtr& operator=(F&& f) {
- FunctionPtr tmp(std::forward
(f)); - swap(*this, tmp);
- return *this;
- }
- // destructor:
- ~FunctionPtr() {
- delete bridge;
- }
- friend void swap(FunctionPtr& fp1, FunctionPtr& fp2) {
- std::swap(fp1.bridge, fp2.bridge);
- }
- explicit operator bool() const {
- return bridge == nullptr;
- }
- // invocation:
- R operator()(Args… args) const; // see functionptr-cpinv.hpp
- };
来自gpt:
explicit operator bool() const
是一种将类类型转换为布尔类型的转换操作符(Conversion operator),也称为 Safe Bool Idiom(安全布尔习惯用法)。
在C++中,可以定义将类类型转换为其他类型的转换操作符。explicit operator bool()
是一种特殊的转换操作符,它将类类型转换为布尔类型 bool
。这种转换操作符的目的是使类的实例能够以布尔值的方式进行条件检查,例如在条件语句中使用类对象。
FunctorBridge 类模板负责持有以及维护底层的函数对象,它被实现为一个抽象基类,为 FunctionPtr 的动态多态打下基础:
- template<typename R, typename… Args>
- class FunctorBridge
- {
- public:
- virtual ~FunctorBridge() {
- }
- virtual FunctorBridge* clone() const = 0;
- virtual R invoke(Args… args) const = 0;
- };
有了这些虚函数,就可以继续实现 FunctionPtr 的拷贝构造函数和函数调用运算符了:
- template<typename R, typename… Args>
- FunctionPtr<R(Args…)>::FunctionPtr(FunctionPtr const& other)
- : bridge(nullptr)
- {
- if (other.bridge) {
- bridge = other.bridge->clone();
- }
- }
- template<typename R, typename… Args>
- R FunctionPtr<R(Args…)>::operator()(Args&&… args) const
- {
- return bridge->invoke(std::forward
(args)…); - }
FunctorBridge 的每一个实例都是一个抽象类,因此其虚函数功能的具体实现是由派生类负责 的。为了支持所有可能的函数对象(一个无界集合),我们可能会需要无限多个派生类。幸 运的是,我们可以通过用其所存储的函数对象的类型(Functor functor)对派生类进行参数化:
- template<typename Functor, typename R, typename… Args>
- class SpecificFunctorBridge : public FunctorBridge
{ - Functor functor;
- public:
- template<typename FunctorFwd>
- SpecificFunctorBridge(FunctorFwd&& functor)
- : functor(std::forward
(functor)) { - }
- virtual SpecificFunctorBridge* clone() const override {
- return new SpecificFunctorBridge(functor);
- }
-
- virtual R invoke(Args&&… args) const override {
- return functor(std::forward
(args)…); - }
- };
每一个 SpecificFunctorBridge 的实例都存储了函数对象的一份拷贝(类型为 Functor),它可 以被调用,拷贝(通过 clone()),以及销毁(通过隐式调用析构函数)。SpecificFunctorBridge 实例会在 FunctionPtr 被实例化的时候顺带产生,FunctionPtr 的剩余实现如下:
- template<typename R, typename… Args>
- template<typename F>
- FunctionPtr<R(Args…)>::FunctionPtr(F&& f)
- : bridge(nullptr)
- {
- using Functor = std::decay_t
; - using Bridge = SpecificFunctorBridge
; - bridge = new Bridge(std::forward
(f)); - }
注意,此处由于 FunctionPtr 的构造函数本身也被函数对象类型模板化了,该类型只为 SpecificFunctorBridge 的特定偏特化版本(以 Bridge 类型别名表述)所知。一旦新开辟的 Bridge 实例被赋值给数据成员 bridge,由于从派生类到基类的转换(Bridge* --> FunctorBridge*),特定类型 F 的额外信息将会丢失。类型信息的丢失,解释了为什么名称“类型 擦除”经常被用于描述用来桥接 static 和 dynamic 多态的技术。
该实现的一个特点是在生成 Functor 的类型的时候使用了 std::decay,这使得被推断出来的类 型 F 可以被存储,比如它会将指向函数类型的引用 decay 成函数指针类型,并移除了顶层 const,volatile 和引用。
上述 FunctionPtr 实现几乎可以被当作一个函数指针的非正式替代品适用。但是它并没有提 供对下面这一函数指针操作的支持:检测两个 FunctionPtr 的对象是否会调用相同的函数。 为了实现这一功能,需要在 FunctorBridge 中加入 equals 操作:
virtual bool equals(FunctorBridge const* fb) const = 0;
在 SpecificFunctorBridge 中的具体实现如下:
- virtual bool equals(FunctorBridge
const* fb) const override - {
- if (auto specFb = dynamic_cast
const*> (fb)) - {
- return functor == specFb->functor;
- }
- //functors with different types are never equal:
- return false;
- }
幸运的是,我们可以使用基于 SFINAE 的萃取技术(见 19.4 节),在调用 operator==之前, 确认它是否可用,如下:
- template<typename T>
- class IsEqualityComparable
- {
- private:
- // test convertibility of == and ! == to bool:
- static void* conv(bool); // to check convertibility to bool
- template<typename U>
- static std::true_type test(decltype(conv(std::declval
- const&>() == std::declvalconst&>())),
- decltype(conv(!(std::declvalconst&>() == std::declval
- const&>()))));
- // fallback:
- template<typename U>
- static std::false_type test(…);
- public:
- static constexpr bool value = decltype(test
(nullptr, - nullptr))::value;
- };
22.6 性能考量
类型擦除技术提供了 static 和 dynamic 多态的一部分优点,但是并不是全部。尤其是,使用 类型擦除技术产生的代码的性能更接近于动态多态,因为它们都是用虚函数实现了动态分 配。因此某些 static 多态的传统优点(比如编译期将函数调用进行 inline 的能力)可能就被 丢掉了。