函数对象、指针与std::function<>
- 函数对象用于给模板提供一些可定制行为,比如下面的函数模板枚举从0到某个值的整型值,每个值被提供给函数对象f
// bridge/forupto1.cpp
#include <vector>
#include <iostream>
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()都将产生一个不同的实例化。这个例子的模板很小,如果十分大则这些实例化可能大大增加代码大小。一个限制代码增长的方法是把函数模板改为不需要实例化的非模板,如使用函数指针
// bridge/forupto2.hpp
void forUpTo(int n, void (*f)(int))
{
for (int i = 0; i != n; ++i)
{
f(i); // call passed function f for i
}
}
forUpTo(5, printInt); // OK: prints 0 1 2 3 4
forUpTo(5,
[&values](int i) { // ERROR: lambda not convertible to a function pointer
values.push_back(i);
});
- 标准库类模板std::function<>提供了一个forUpTo()可选用的构建
// bridge/forupto3.hpp
#include <functional>
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::function<>的模板实参是一个函数类型,它描述函数对象将接收的参数类型和产生的返回类型。这个forUpTo()的构建提供了一些静态多态方面的特点——能够处理无界集合的类型,包括函数指针、lambda、任意带合适的operator()的类。它使用类型擦除的技术做到这点,类型擦除桥接了静态多态与动态多态的间隔
广义的函数指针
- std::function<>类型是一个广义的函数指针形式,它提供了同样的基本操作
- 可用于调用函数而不需要调用者知道关于函数本身的任何东西
- 可以被拷贝、移动和赋值
- 可由另一个函数初始化或赋值
- 有一个null状态来表明没有绑定函数
- 然而不同于函数指针的是,std::function<>也能存储一个lambda或其他任何带有合适的operator()的函数对象
- 接下来建立一个广义函数指针类模板FunctionPtr,它同样会提供这些核心操作并能用于代替std::function
// bridge/forupto4.cpp
#include "functionptr.hpp"
#include <vector>
#include <iostream>
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的接口是很明显的,需要提供构造、拷贝、移动、析构、初始化、赋值、调用等操作。最有趣的部分是如何在一个类模板局部特化中描述接口,以用于将模板实参(一个函数类型)分解为其组成部分(结果和实参类型)
// bridge/functionptr.hpp
// primary template:
template<typename Signature>
class FunctionPtr;
// partial specialization:
template<typename R, typename... Args>
class FunctionPtr<R(Args...)>
{
private:
FunctorBridge<R, Args...>* bridge;
public:
// constructors:
FunctionPtr() : bridge(nullptr) {
}
FunctionPtr(FunctionPtr const& other); // see functionptrcpinv.hpp
FunctionPtr(FunctionPtr& other)
: FunctionPtr(static_cast<FunctionPtr const&>(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>(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
};
- 这个实现包含一个单独的非静态成员变量bridge,它将负责存储的函数对象的存储的操作。这个指针的所有权绑定到FunctionPtr对象上,因此提供的大多实现只是管理这个指针
桥接口
- FunctorBridge类模板负责函数对象的所有权和操作,它实现为一个抽象基类,构成FunctionPtr动态多态的基础
// bridge/functorbridge.hpp
template<typename R, typename... Args>
class FunctorBridge
{
public:
virtual ~FunctorBridge() {
}
virtual FunctorBridge* clone() const = 0;
virtual R invoke(Args... args) const = 0;
};
- FunctorBridge通过虚函数提供必要的操作:析构函数、clone()、invoke()
// bridge/functionptr-cpinv.hpp
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>(args)...);
}
类型擦除(Type Erasure)
- FunctorBridge实例是一个抽象类,因此派生类负责提供虚函数的具体实现。为了支持整个范围的潜在函数对象——一个无界集合——需要无界数量的派生类。要实现这点,可基于派生类存储的函数对象类型,参数化派生类
// bridge/specificfunctorbridge.hpp
template<typename Functor, typename R, typename... Args>
class SpecificFunctorBridge : public FunctorBridge<R, Args...> {
Functor functor;
public:
template<typename FunctorFwd>
SpecificFunctorBridge(FunctorFwd&& functor)
: functor(std::forward<FunctorFwd>(functor)) {
}
virtual SpecificFunctorBridge* clone() const override {
return new SpecificFunctorBridge(functor);
}
virtual R invoke(Args... args) const override {
return functor(std::forward<Args>(args)...);
}
};
- SpecificFunctorBridge的实例化存储一个函数对象的拷贝,它能被调用、拷贝或析构。无论何时一个FunctionPtr初始化为一个新函数对象,都将创建一个SpecificFunctorBridge实例
// bridge/functionptr-init.hpp
template<typename R, typename... Args>
template<typename F>
FunctionPtr<R(Args...)>::FunctionPtr(F&& f)
: bridge(nullptr)
{
using Functor = std::decay_t<F>;
using Bridge = SpecificFunctorBridge<Functor, R, Args...>;
bridge = new Bridge(std::forward<F>(f));
}
- 注意FunctionPtr构造函数本身基于函数对象类型F模板化,那个类型只被SpecificFunctorBridge的局部特化知道。一旦新分配的Bridge实例赋值给数据成员bridge,关于特定类型F的额外信息就会因派生类到基类的转换(Bridge*到 FunctorBridge<R, Args...> *)而丢失。这个类型信息的丢失解释了为何术语“类型擦除”常用于描述桥接静态与动态多态的技术
可选桥接
- FunctionPtr还不支持函数指针提供的一个操作:测试是否两个FunctionPtr对象将调用相同的函数。添加这样一个操作需要FunctorBridge更新一个equals操作
virtual bool equals(FunctorBridge const* fb) const = 0;
- 接着在SpecificFunctorBridge中实现
virtual bool equals(FunctorBridge<R, Args...> const* fb) const override
{
if (auto specFb = dynamic_cast<SpecificFunctorBridge const*> (fb))
{
return functor == specFb->functor;
}
// functors with different types are never equal:
return false;
}
- 最终为FunctionPtr实现operator==
friend bool
operator==(FunctionPtr const& f1, FunctionPtr const& f2) {
if (!f1 || !f2) {
return !f1 && !f2;
}
return f1.bridge->equals(f2.bridge);
}
friend bool
operator!=(FunctionPtr const& f1, FunctionPtr const& f2) {
return !(f1 == f2);
}
- 这个实现是正确的,但有一个缺点:如果FunctionPtr用没有operator==的函数对象赋值或初始化,编译将报错。这可能带来意外,因为FunctionPtr的operator==还没被使用,而许多其他类模板,如std::vector可以用没有operator==的类型实例化,只要operator==没被使用
- 这个问题来源于类型擦除:因为一旦FunctionPtr被赋值或初始化,实际上就丢失了函数对象的类型,这就需要在赋值或实例化完成前捕获所有需要知道的类型信息。这个信息包括构建一个函数对象的operator==的调用,因为不能确定何时需要它
- 可以用SFINAE-based traits在调用前查询operator==是否可用
// bridge/isequalitycomparable.hpp
#include <utility> // for declval()
#include <type_traits> // for true_type and false_type
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<U const&>() ==
std::declval<U const&>())),
decltype(conv(!(std::declval<U const&>() ==
std::declval<U const&>())))
);
// fallback:
template<typename U>
static std::false_type test(...);
public:
static constexpr bool value = decltype(test<T>(nullptr, nullptr))::value;
};
- IsEqualityComparable用了典型的表达式测试的构成:两个test()重载,一个包含包裹在decltype中需要测试的表达式,另一个接收任意数量实参
- 使用IsEqualityComparable可以构造一个TryEquals类模板,可以调用给定类型的==或在没有合适的==时抛出异常
// bridge/tryequals.hpp
#include <exception>
#include "isequalitycomparable.hpp"
template<typename T,
bool EqComparable = IsEqualityComparable<T>::value>
struct TryEquals
{
static bool equals(T const& x1, T const& x2) {
return x1 == x2;
}
};
class NotEqualityComparable : public std::exception
{
};
template<typename T>
struct TryEquals<T, false>
{
static bool equals(T const& x1, T const& x2) {
throw NotEqualityComparable();
}
};
- 最终,通过使用TryEquals就能在FunctionPtr中提供对==的支持
virtual bool equals(FunctorBridge<R, Args...> const* fb) const override
{
if (auto specFb = dynamic_cast<SpecificFunctorBridge const*> (fb))
{
return TryEquals<Functor>::equals(functor, specFb->functor);
}
// functors with different types are never equal:
return false;
}
性能考虑
- 类型擦除同时提供了一些但非全部的静态多态和动态多态的优点。特别地,使用类型擦除的泛型代码的性能更接近于动态多态,因为两者都通过虚函数使用动态调度。因此,一些传统的静态多态的优点,如编译器内联调用的能力,可能丢失。这种性能损失是否可感知是依赖于应用程序的,但通常很容易判断,考虑相对虚函数调用开销需要执行多少工作:如果两者接近,如使用FunctionPtr相加两个整数,类型擦除可能执行得比静态多态版本慢得多,反之如果函数调用执行大量工作,如查询数据库、排序容器或更新用户接口,类型擦除的开销不太可能是可测量的
网友评论