65.9K
CodeProject 正在变化。 阅读更多。
Home

轻量级通用 C++ 回调(或者,又一个委托实现)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (54投票s)

2010年12月15日

CPOL

9分钟阅读

viewsIcon

112538

downloadIcon

1999

一个快速、符合标准且易于使用的 C++ 回调实现

引言

C++ 委托的实现已经做了大量工作,这从许多相关的问题和文章(在 Stack Overflow 和 The Code Project 等网站上很容易找到)可以看出。尽管付出了努力,但一个特定的 Stack Overflow 问题[1] 表明,即便过了这么多年,人们对它仍然感兴趣。具体来说,该问题提问者寻求一个快速、符合标准且易于使用的 C++ 委托实现。

C++ 中的委托可以通过函数指针来实现。Don Clugston 的文章[2] 深入探讨了这一主题,Clugston 的研究表明,成员函数指针的大小并非都相同,这使得创建能够直接处理成员函数指针的委托机制变得困难。尽管如此,Clugston 提供了一种方法,通过深入了解最常用编译器内部代码生成方案来实现 C++ 委托。虽然可行,但它不满足符合标准的要求。

正是由于这个原因,Boost Function 库[3] 在内部使用自由存储来存储成员函数指针。虽然这是解决问题最明显的方法,但它会增加显著的运行时开销,这不符合速度要求。

Sergey Ryazanov 的文章[4] 提供了一个符合标准且快速的解决方案,使用了标准的 C++ 模板。然而,Ryazanov 实现中实例化委托的语法混乱且冗余,因此不满足易用性的要求。

我提供了一个概念验证委托实现,它满足所有三个作为上述 Stack Overflow 问题的答案。本文将更详细地讨论我的答案。

Using the Code

我提供的源代码支持全局函数、静态函数和成员函数,基于我将在本文后面介绍的委托机制。以下代码片段演示了这一点

using util::Callback; // Callback lives in the util namespace

class Foo
{
public:
    Foo() {}

    double MemberFunction(int a, int b)            { return a+b; }
    double ConstMemberFunction(int a, int b) const { return a-b; }
    static double StaticFunction(int a, int b)     { return a*b; }
};

double GlobalFunction(int a, int b) { return a/(double)b; }

double Invoke(int a, int b, Callback<double (int, int)> callback)
{
    if(callback) return callback(a, b);
    return 0;
}

int main()
{
    Foo f;

    Invoke(10, 20, BIND_MEM_CB(&Foo::MemberFunction, &f));      // Returns 30.0
    Invoke(10, 20, BIND_MEM_CB(&Foo::ConstMemberFunction, &f)); // Returns -10.0
    Invoke(10, 20, BIND_FREE_CB(&Foo::StaticFunction));         // Returns 200.0
    Invoke(10, 20, BIND_FREE_CB(&GlobalFunction));              // Returns 0.5
    
    return 0;
}

BIND_MEM_CBBIND_FREE_CB 宏展开为返回一个绑定到传入函数的 Callback 对象的表达式。对于成员函数,还会传入一个实例的指针。生成的 Callback 对象可以像函数指针一样被调用。

请注意使用“首选语法”来指定函数签名。例如,Callback<double (int, int)> 是一个 Callback 对象的类型,它可以绑定到接受两个 int 参数并返回 double 的函数。这也意味着调用 Callback<double (int, int)> 对象需要两个 int 参数并返回一个 double

另请注意,宏 BIND_MEM_CB 可以接受 const 或非 const 的成员函数。如果传入的函数指针指向一个 const 成员函数,BIND_MEM_CB 只接受 const T* 实例指针。否则,它接受带有非 const 成员函数的 T*。因此,回调机制是“const 正确性”感知的。全局函数和 static 函数都通过 BIND_FREE_CB 宏绑定到回调对象。在任何一种情况下,提供的库都支持接受 0 到 6 个参数的函数。

由于回调机制不依赖于自由存储,因此可以轻松存储和复制(只要它们的函数签名匹配)。Callback 对象可以被视为布尔值(通过安全布尔语法[5]),以测试它是否绑定到函数,正如上面示例代码中的 Invoke() 函数所示。由于机制的工作方式,无法比较两个 Callback 对象。尝试这样做会导致编译错误。

可以通过将 NullCallback 的实例赋值给对象来解除 Callback 对象的绑定(恢复到默认状态)

callbackObj = NullCallback();

局限性和编译器可移植性

该库采用“极简”方法设计。Callback 对象不跟踪对象生命周期——因此,调用绑定到已删除或超出作用域的对象上的 Callback 对象会导致未定义行为。与 Boost.Function 不同,函数签名必须完全匹配——无法绑定签名“接近但并非精确”的函数。该库并非旨在替代 Boost.Function——它旨在成为一个方便的轻量级替代品。

尽管代码不需要 C++0x 特性,并且只使用了我认为是标准 C++ 的内容,但代码确实需要一个功能相当强大(近期)的编译器。代码在 Visual C++ 6.0 等编译器上将无法正常工作。

话虽如此,该代码已在 Visual C++ 8.0 SP1(版本 14.00.50727.762)、Visual C++ 9.0 SP1(版本 15.00.30729.01)和 GCC C++ 编译器版本 4.5.0(通过 MinGW)上成功测试。代码在 Visual C++ 上使用 /W4,在 GCC 上使用 -Wall 编译,都可以干净地通过。

工作原理

理解底层机制的最佳方法是从一个非常原始和简单的委托实现开始,然后在此基础上进行扩展。考虑一个牵强的回调实现

float Average(int n1, int n2)
{     
    return (n1 + n2) / 2.0f;
}

float Calculate(int n1, int n2, float (*callback)(int, int))
{
    return callback(n1, n2);
}

int main()
{
    float result = Calculate(50, 100, &Average);
    // result == 75.0f
    return 0;
}

这对于指向全局函数(以及指向 static 函数)的指针来说效果很好,但对于指向成员函数的指针则完全无效。同样,这是由于成员函数指针大小的不同,如 Clugston 的文章所示。由于“计算机科学中的所有问题都可以通过另一层间接寻址来解决”,可以创建一个包装函数,该函数兼容这样的回调接口,而不是直接传递成员函数指针。因为成员函数指针需要一个对象来调用,所以还应该修改回调接口以接受任何对象的 void* 指针

class Foo
{
public:
    float Average(int n1, int n2)
    {     
        return (n1 + n2) / 2.0f;
    }
};

float FooAverageWrapper(void* o, int n1, int n2)
{     
    return static_cast<Foo*>(o)->Average(n1, n2);
}

float Calculate(int n1, int n2, float (*callback)(void*, int, int), void* object)
{
    return callback(object, n1, n2);
}

int main()
{
    Foo f;
    float result = Calculate(50, 100, &FooAverageWrapper, &f);
    // result == 75.0f
    return 0;
}

这种“解决方案”适用于任何类的任何方法,但每次都需要编写包装函数,这很麻烦,所以最好尝试推广和自动化这个解决方案。可以将包装函数写成模板函数。此外,由于成员函数指针和对象指针必须成对出现,因此可以将这两个指针存储在一个专用对象中。让我们提供一个 operator()(),这样该对象就可以像函数指针一样被调用

template<typename R, typename P1, typename P2>
class Callback
{
public:
    typedef R (*FuncType)(void*, P1, P2);
    Callback() : func(0), obj(0) {}
    Callback(FuncType f, void* o) : func(f), obj(o) {}
    R operator()(P1 a1, P2 a2)
    {
        return (*func)(obj, a1, a2);
    }
    
private:
    FuncType func;
    void* obj;
};

template<typename R, class T, typename P1, typename P2, R (T::*Func)(P1, P2)>
R Wrapper(void* o, P1 a1, P2 a2)
{
    return (static_cast<T*>(o)->*Func)(a1, a2);
}

class Foo
{
public:
    float Average(int n1, int n2)
    {
        return (n1 + n2) / 2.0f;
    }
};

float Calculate(int n1, int n2, Callback<float, int, int> callback)
{
    return callback(n1, n2);
}

int main()
{
    Foo f;
    Callback<float, int, int> cb         
        (&Wrapper<float, Foo, int, int, &Foo::Average>, &f);
    float result = Calculate(50, 100, cb);
    // result == 75.0f
    return 0;
}

通过利用 C++ 模板的“非类型模板参数”特性,包装函数已被通用化,可以接受函数指针。当实例化包装函数时(通过获取其地址),编译器能够生成在编译时直接调用包装函数中模板参数指向的函数的代码。由于包装函数在此代码中是全局函数,因此可以轻松地将其存储在 Callback 对象中。

这实际上是 Ryazanov 委托实现的基础[4]。现在应该清楚为什么 Ryazanov 的解决方案未能满足易用性的要求——实例化包装函数以创建 Callback 对象所需的语法不自然且冗余。因此,还需要做更多工作。

奇怪的是,编译器无法直接从函数指针本身推断出构成它的类型。唉,C++ 标准不允许这样做[6]

无法从非类型模板参数的类型中推断出模板类型参数。 [示例:
template<class T, T i> void f(double a[10][i]);
int v[10][20];
f(v); // error: argument for template-parameter T cannot be deduced

结束示例]

必须使用另一种推导方法。众所周知,函数调用的模板参数可以进行推导,因此让我们探索使用一个虚拟函数来推导传入的函数指针的类型的可能性

template<typename R, class T, typename P1, typename P2>
void GetCallbackFactory(R (T::*Func)(P1, P2)) {}

类型 R, T, P1,P2 在函数内部可用。要将它们“带出”函数,可以返回一个虚拟对象,并将推导出的类型“编码”到该虚拟对象本身的类型中

template<typename R, class T, typename P1, typename P2>
class MemberCallbackFactory
{
};

template<typename R, class T, typename P1, typename P2>
MemberCallbackFactory<R, T, P1, P2> GetCallbackFactory(R (T::*Func)(P1, P2))
{
    return MemberCallbackFactory<R, T, P1, P2>();
}

由于虚拟对象“知道”推导出的类型,让我们将包装函数和 Callback 对象创建代码移到其中

template<typename R, class T, typename P1, typename P2>
class MemberCallbackFactory
{
private:
    template<R (T::*Func)(P1, P2)>
    static R Wrapper(void* o, P1 a1, P2 a2)
    {
        return (static_cast<T*>(o)->*Func)(a1, a2);
    }

public:
    template<R (T::*Func)(P1, P2)>
    static Callback<R, P1, P2> Bind(T* o)
    {
        return Callback<R, P1, P2>(&MemberCallbackFactory::Wrapper<Func>, o);
    }
};

template<typename R, class T, typename P1, typename P2>
MemberCallbackFactory<R, T, P1, P2> GetCallbackFactory(R (T::*Func)(P1, P2))
{
    return MemberCallbackFactory<R, T, P1, P2>();
}

然后,可以调用 Bind<>() 上的临时对象,该临时对象是从 GetCallbackFactory() 返回的

int main()
{
    Foo f;
    Callback<float, int, int> cb = 
	GetCallbackFactory(&Foo::Average).Bind<&Foo::Average>(&f);
}

请注意,Bind<>() 实际上是一个 static 函数。C++ 标准允许在实例上调用 static 函数[7]

Xstatic 成员 s 可以使用限定 ID 表达式 X::s 来引用;引用静态成员不必使用类成员访问语法 (5.2.5)。可以使用类成员访问语法引用 static 成员,在这种情况下,将评估对象表达式。 [示例:
class process {
public:
        static void reschedule();
}
process& g();
void f()
{
        process::reschedule(); // OK: no object necessary
        g().reschedule();      // g() is called
}

结束示例]
....

当编译器遇到上面表达式中的 Bind<>() 调用时,编译器会评估 GetCallbackFactory(),这有助于推导出函数指针的类型。一旦完成推导,就会返回相应的 Callback 工厂,然后可以将函数指针传递给 Bind<>(),而无需显式提供各个类型。调用 Bind<>() 会生成一个 Callback 对象。

最后,提供了一个简单的宏来简化表达式。由于宏会展开为实际的模板函数调用,因此该机制即使使用了宏也是类型安全的。

#define BIND_MEM_CB(memFuncPtr, instancePtr) 
	(GetCallbackFactory(memFuncPtr).Bind<memFuncPtr>(instancePtr))

int main()
{
    Foo f;
    float result = Calculate(50, 100, BIND_MEM_CB(&Foo::Average, &f));
    // result == 75.0f
    return 0;
}

这实际上完成了委托机制。检查反汇编(从优化构建中)表明,回调机制涉及的不仅仅是指针赋值。根据绑定到回调对象的函数,目标函数可能会内联到包装函数本身。但由于存在额外的间接级别,最好通过引用传递“大”对象。否则,对于回调来说应该足够快了。

结论

可以实现一个快速、符合标准且具有简单语法的委托系统。C++ 语言具备实现这一目标所需的设施,并且对于功能足够强大的编译器,它们可以用于实现 C++ 委托。本文提出的解决方案应足以满足大多数应用程序的需求。

历史

  • 版本 0.1 - 2010 年 11 月 29 日:概念验证
  • 版本 1.0 - 2010 年 12 月 14 日:初始发布

参考文献

  1. ^ 5 年后,有没有比“最快的 C++ 委托”更好的东西? Stack Overflow。 2010 年 11 月 28 日
  2. ^ Don Clugston。 成员函数指针和最快的 C++ 委托。The Code Project。2004 年 5 月 23 日。
  3. ^ Douglas Gregor。 第 7 章 Boost.Function。Boost。2004 年 7 月 25 日。
  4. ^ a b Sergey Ryazanov。 极快的 C++ 委托。The Code Project。2005 年 7 月 17 日。
  5. ^ Bjorn Karlsson。 安全布尔语法。Artima Developer。2004 年 7 月 31 日。
  6. ^ ISO/IEC (2003)。ISO/IEC 14882:2003(E):编程语言 - C++ §14.8.2.4 从类型推导模板参数 [temp.deduct.type] para. 12。
  7. ^ ISO/IEC (2003)。ISO/IEC 14882:2003(E):编程语言 - C++ §9.4 静态成员 [class.static] para. 2。
© . All rights reserved.