一个智能函数指针






4.44/5 (9投票s)
一个智能函数指针。
引言
C++ 没有标准的通用函数指针,它支持相等性比较,并且可以指向独立函数、成员函数以及 Lambda/函数对象,只要它们共享一个共同的函数签名。我提出了一个名为 fun_ptr 的类型,可以做到这一点。对于成员函数目标,还可以选择存储一个对象实例指针(参见 fun_ptr
构造函数)。这在其他语言中有时被称为委托(delegate)。相等性比较将得到支持,并产生直观的结果。相等的唯一要求是用户定义的函数对象必须提供 operator==。对于所有其他目标(包括 Lambda 函数),相等性会自动生效。如果对于某个用户定义的函数对象不需要相等性,则可以省略定义 operator==。
为什么不使用 std::function + std::bind?
确实,`std::function` + `std::bind` 的组合能够指向独立函数、成员函数和函数对象。然而,`std::function` 存在 const 性问题(参见 N4159),缺乏相等性比较,因为它“无法很好地实现”(参见 boost faq),并且函数签名匹配“松散”。相等性比较是智能函数指针应该支持的一个基本操作。Herb Sutter 在《Generalizing Observer》中指出,使用 std::function 实现观察者模式存在一个问题,即缺乏相等性比较。Boost 的 signals2 库通过返回一个句柄来解决这个问题,该句柄可用于稍后分离。当一个对象负责连接和断开时,这效果很好。然而,当连接和断开可能发生在任何数量的位置时,连接句柄必须成为一个共享对象。如果仅通过知道连接目标就可以执行断开操作,那就更方便了。此外,将 std::function 与 `std::bind` 结合使用会产生相当丑陋的语法。
fun_ptr
通过实现相等性比较、严格强制执行函数签名以及提供更令人愉悦的语法,改进了 `std::function` 和 `std::bind`。此外,`make_fun` 辅助函数简化了 `fun_ptr` 对象的创建(参见示例)。
定义
由于期望实现相等性比较,因此了解相等性的含义很重要。John Lakos 有一个关于值含义的演讲(参见 YouTube:Value Semantics: It ain't about the syntax)。基本来说,值类型应该具有三个属性
- 可被副本替换
- 如果 A 和 B 的值相同,并且对 A 和 B 执行了相同的关键操作,那么这两个对象将再次具有相同的值。
- 给定值语义类型的两个对象,当且仅当在其所有关键操作中不存在区分序列时,它们才具有相同的值。
关键操作的定义基本上是任何对类型的值有影响的操作。Lakos 举的例子是 `std::vector` 的大小是一个关键属性,但其容量不是。因此 `resize()`、`push_back()` 等是关键操作,而 `reserve()` 和 `shrink_to_fit()` 则不是。
智能函数指针描述
fun_ptr 是一种值类型,其唯一关键属性是其目标。目标是函数(独立函数、成员函数、函数对象/Lambda 或 nullptr)以及适用的对象实例指针。与 `std::function` 不同,fun_ptr 的设计具有函数指针的语义,并支持相等性操作。通过 `operator()` 调用目标。不允许使用 nullptr 目标调用 fun_ptr
。operator= 更改 fun_ptr 的目标,并且是可以在 fun_ptr
对象上执行的唯一关键的修改操作(注意:重要的是要注意 operator()
不是关键操作)。
相等性比较。指向相同对象实例(如果适用)和成员函数的 fun_ptrs
将比较相等。具有不同目标类型的两个 fun_ptr(例如,可变成员函数和 const 成员函数)将比较不相等。具有不同对象实例的两个 fun_ptr 将比较不相等。如果目标类型和对象实例相同,但其中一个或两个成员函数是虚函数,则相等性比较结果未定义(因为 C++ 规范未指定虚函数指针比较的结果)。如果用户定义的函数对象未实现 operator==,则相等性比较结果未定义。一种可能的确定相等性的算法如下:
- 如果目标是不同的类型,则返回 false
- 否则,如果两个目标都是 nullptr,则返回 true
- 否则,如果两个目标都是独立函数
- 返回函数指针的比较结果
- 否则,如果两个目标都是成员函数(无实例指针)
- 如果成员函数指向不同的类,则返回 false
- 否则,返回成员函数指针的比较结果
- 否则,如果两个目标都是带实例指针的成员函数指针
- 如果目标是不同的类,则返回 false
- 否则,如果实例指针不同,则返回 false
- 否则,返回函数指针的比较结果(如果至少有一个是虚函数,则未定义)
- 否则,如果两个目标都是带有 operator== 的函数对象
- 返回 operator== 的结果
- 否则,如果两个目标都是没有 operator== 的函数对象
- 如果函数对象类型不同,则返回 false
- 否则(函数对象类型相同),则返回 true
fun_ptr 不允许指向可变 Lambda 函数,因为它会违反函数指针语义并可能导致线程安全问题(参见 N4159)。C++ 还保证每个定义的 Lambda 函数都有一个唯一的类型。结合这两项,fun_ptr 可以正确处理 Lambda 函数的相等性(步骤 7a 和 7b)。这是因为类型本身足以确定非可变 Lambda 函数之间的相等性。
按照惯例(参见 Meyers 的 Effective STL),函数对象(或 Lambda)类型是通过值传递的(通常是临时对象)。当 fun_ptr 使用函数对象构造时,它会将函数对象的副本作为 fun_ptr 的成员来构造。
对于成员函数目标,用户有责任确保目标引用的是一个有效对象。也就是说,不允许使用不再存在的 target 调用 fun_ptr
。fun_ptr
具有类似函数指针的浅 const 和浅拷贝语义。
可能会问:fun_ptr 是线程安全的吗?回想一下,fun_ptr 的设计是为了具有函数指针般的语义,所以答案与“调用函数指针是否线程安全?”这个问题相同。这取决于它所指向函数的实现。对于独立函数,该函数需要是可重入的。对于成员函数或函数对象,它需要在内部进行同步。
智能函数指针实现
我已经实现了 fun_ptr 的两个独立版本。第一个版本使用动态内存分配和 RTTI。第二个版本对除了大型函数对象之外的所有内容都使用了小型对象优化,并使用模板技巧来移除 RTTI。因此,第二个实现的性能比第一个版本要快一个数量级(对于大型函数对象是个例外,因为它们仍然需要动态内存分配来构造)。
性能。为了完成此性能测试,我包含了 Don Clugston 的 fastdelegate 和我的另一个库(基于 Elbert Mai 的轻量级回调(Delegate) 的 Delegate)。Delegate 的速度非常快,但有一些不理想的特性:它使用宏进行创建,不支持函数对象,并且对于依赖的模板函数名称需要古怪的语法。Don Clugston 的 fastdelegate 是另一个流行的代理,也非常快,但使用了一些非标准的 C++,并且不支持函数对象。正如调用基准测试所示,所有实现的调用性能相似。它们之间的区别在于创建、销毁、赋值、相等性检查以及支持的目标。
这些测试是在 gcc4.7.3 下进行的。
fastdelegate | 委托 | fun_ptr2 | fun_ptr3 | std::function+std::bind | |
大小(字节) | 12 | 8 | 12 到 16+ | 20+ | 16 + ??? |
基准测试 1(秒) | 1.15 | 1.033 | 34.068 | 3.6 | 不支持相等性 |
基准测试 2(秒) | 1.142 | 1.016 | 24.318 | 3.21 | 25.998 |
基准测试 3(秒) | 0.603 | 0.536 | 0.947 | 0.986 | 1.025 |
lambda | 否 | 否 | 是 | 是 | 是 |
成员函数 | 是 | 是 | 是 | 是 | 是 |
非成员函数 | 是 | 是 | 是 | 是 | 是 |
注释:加号表示大型函数对象需要更多 |
基准测试 1:创建、销毁、赋值、相等性检查、调用 |
基准测试 2:创建、销毁、赋值、调用 |
基准测试 3:仅调用 |
观察者模式 / 事件
有关观察者模式以及使用 std::function
问题的更详细讨论,请参阅 Herb Sutter 的 Generalizing Observer。
有了 fun_ptr 及其 operator== 的使用,观察者模式就可以在无需诉诸 cookie 或句柄来分离观察者的情况下实现。下面的代码是真实的并且有效。在我看来,这段代码的语法与 C# 中委托和事件的原生实现相当。
class High_score
{
public:
//a public event object
event<void(int)> newHighScoreEvent;
High_score()
: newHighScoreEvent()
, newHighScoreEventInvoker(newHighScoreEvent.get_invoker())
, high_score(0)
{
}
void submit_score(int score)
{
if (score > high_score) {
high_score = score;
newHighScoreEventInvoker(high_score); //invoke the event
}
}
private:
//a private invoker for the event (only this class may invoke the event)
decltype(newHighScoreEvent)::invoker_type newHighScoreEventInvoker;
int high_score;
};
void print_new_high_score(int newHighScore)
{
std::cout << "new high score = " << newHighScore << "\n";
}
int main()
{
High_score hs;
//can use a member function or lambda as well
hs.newHighScoreEvent.attach(make_fun(&print_new_high_score));
//submit a few scores
hs.submit_score(1);
hs.submit_score(3);
hs.submit_score(2);
hs.newHighScoreEvent.detach(make_fun(&print_new_high_score));
//no effect because observer is detached
hs.submit_score(4);
}
参考文献
- 成员函数指针和最快的 C++ 委托 作者:Don Clugston
- 不可能快的 C++ 委托 作者:Sergey Ryazanov
- std::function and Beyond (N4159) 作者:Geoffrey Romer 和 Roman Perepelitsa
- 值语义:与语法无关! 作者:John Lakos
- Generalizing Observer 作者:Herb Sutter
- Signals2 来自 Boost
- 轻量级通用 C++ 回调(或,又一个委托实现) 作者:Elbert
- Mai
智能函数指针示例
示例 1:函数对象相等性
auto lam = []() { /*...*/ };
auto f1 = make_fun(lam);
auto f2 = f1; //copy constructor
assert(f2 == f1); //always true after a copy constructor
f1(); //invoke operator() const
assert(f2 == f1); //since operator() is const equality was not changed
f2 = make_fun(lam); //create another lambda
assert(f1 == f2);
f1 = f2; //copy assignment
assert(f1 == f2); //always true after a copy assignment
示例 2:成员函数对象相等性
auto f1 = make_fun(&Some_class::some_mem_fun, &some_class);
auto f2 = d1; //copy constructor
assert(f1 == f2); //always true after a copy constructor
f1(); //invoke operator() const
assert(f2 == f1); //since operator() is const equality was not changed
f2 = make_fun(&Some_class::some_mem_fun, &some_class);
assert(f1 == f2); //target is now the same so equal
f1 = f2; //copy assignment
assert(f1 == f2); //always true alfter a copy assignment
fun_ptr 接口
这是 fun_ptr
的接口,不包含实现细节。
#ifndef FUN_PTR_INTERFACE_H
#define FUN_PTR_INTERFACE_H
template <typename FuncSignature>
class fun_ptr;
template <typename Ret, typename... Args>
class fun_ptr<Ret(Args...)>
{
public:
fun_ptr() noexcept;
fun_ptr(std::nullptr_t) noexcept;
// not declared noexcept because of a narrow contract and optionally dynamic
// memory allocation is allowed.
fun_ptr(Ret (*func)(Args...));
// not declared noexcept because of a narrow contract and optionally dynamic
// memory allocation is allowed.
template <typename T>
fun_ptr(Ret (T::*method)(Args...), T* object);
// not declared noexcept because of a narrow contract and optionally dynamic
// memory allocation is allowed.
template <typename T>
fun_ptr(Ret (T::*method)(Args...) const, const T* object);
// not declared noexcept because of a narrow contract and optionally dynamic
// memory allocation is allowed.
template <typename T, typename... PArgs>
fun_ptr(Ret (T::*mem_fun_ptr)(PArgs...));
// not declared noexcept because of a narrow contract and optionally dynamic
// memory allocation is allowed.
template <typename T, typename... PArgs>
fun_ptr(Ret (T::*mem_fun_ptr)(PArgs...) const);
// not declared noexcept because optionally dynamic memory allocation is
// allowed.
template <typename T>
fun_ptr(T f);
fun_ptr(const fun_ptr& rhs);
fun_ptr(fun_ptr&& rhs) noexcept;
~fun_ptr() noexcept;
fun_ptr& operator=(std::nullptr_t) noexcept;
fun_ptr& operator=(const fun_ptr& rhs);
fun_ptr& operator=(fun_ptr&& rhs) noexcept;
explicit operator bool() const noexcept;
template <typename... FwArgs>
Ret operator()(FwArgs... args) const;
};
template <typename Ret, typename... Args>
inline bool operator==(const fun_ptr<Ret(Args...)>& lhs, const fun_ptr<Ret(Args...)>& rhs) noexcept;
template <typename Ret, typename... Args>
inline bool operator!=(const fun_ptr<Ret(Args...)>& lhs, const fun_ptr<Ret(Args...)>& rhs) noexcept;
template <typename Ret, typename... Args>
inline bool operator==(const fun_ptr<Ret(Args...)>& lhs, std::nullptr_t) noexcept;
template <typename Ret, typename... Args>
inline bool operator!=(const fun_ptr<Ret(Args...)>& lhs, std::nullptr_t) noexcept;
template <typename Ret, typename... Args>
inline bool operator==(std::nullptr_t, const fun_ptr<Ret(Args...)>& rhs) noexcept;
template <typename Ret, typename... Args>
inline bool operator!=(std::nullptr_t, const fun_ptr<Ret(Args...)>& rhs) noexcept;
template <typename Ret, typename... Args>
inline fun_ptr<Ret(Args...)> make_fun(Ret (*fp)(Args...));
template <typename Ret, typename T, typename... Args>
inline fun_ptr<Ret(Args...)> make_fun(Ret (T::*fp)(Args...), T* obj);
template <typename Ret, typename T, typename... Args>
inline fun_ptr<Ret(Args...)> make_fun(Ret (T::*fp)(Args...) const, const T* obj);
template <typename T>
inline auto make_fun(T functor) -> decltype(make_fun(&T::operator(), (T*) nullptr));
#endif // FUN_PTR_INTERFACE_H
event 接口
这是 event 的接口,不包含实现细节。
template <typename FuncSignature>
class event;
template <typename Ret, typename... Args>
class event<Ret(Args...)>
{
public:
event(const event&) = delete;
event(event&&) = delete;
event& operator=(const event&) = delete;
event& operator=(event&&) = delete;
typedef util3::fun_ptr<Ret(Args...)> delegate_type;
typedef delegate_type invoker_type;
event();
//! @brief retrieve the invoker for this event.
//!
//! This function can only be called once.
invoker_type get_invoker();
void attach(const delegate_type& theDelegate);
void detach(const delegate_type& theDelegate);
};