什么是 C++ 代理(delegate)?

178

在C++中委托的一般概念是什么?它们是什么,如何使用它们以及它们的用途是什么?

我想首先用一个“黑盒子”方式学习它们,但是了解这些东西的内部机理也很好。

虽然这不是C ++最纯净或最清晰的表现方式,但我注意到我工作的代码库中有大量这样的使用。我希望足够理解它们,以便我只需使用它们而不必深入研究可怕的嵌套模板。

这两篇The Code Project文章解释了我的意思,但并不特别简洁:


4
你是在谈论.NET下的托管C++吗? - Sergey Kalinichenko
2
你看过委托(编程)的维基百科页面吗? - Matthijs Bierman
6
在C++中,“delegate”不是常见的术语。您应该添加一些信息到问题中,以包括您读到它的上下文。请注意,虽然模式可能很常见,但是如果您谈论“delegate”在一般情况下还是在特定实现的C++CLI或任何其他库的上下文中,答案可能会有所不同。 - David Rodríguez - dribeas
6
等两年?;) - rank1
6
仍在等待中... - Grimm The Opiner
显示剩余2条评论
6个回答

207

在C++中,您有很多选择来实现委托。以下是我想到的。


选项1:函数对象:

通过实现operator()可以创建一个函数对象。

struct Functor
{
     // Normal class/struct members

     int operator()(double d) // Arbitrary return types and parameter list
     {
          return (int) d + 1;
     }
};

// Use:
Functor f;
int i = f(3.14);

选项2:Lambda表达式(仅限C++11

// Syntax is roughly: [capture](parameter list) -> return type {block}
// Some shortcuts exist
auto func = [](int i) -> double { return 2*i/1.15; };
double d = func(1);

选项三:函数指针

int f(double d) { ... }
typedef int (*MyFuncT) (double d);
MyFuncT fp = &f;
int a = fp(3.14);

选项4:成员函数指针(最快的解决方案)

请参阅 Fast C++ Delegate(在The Code Project上)。

struct DelegateList
{
     int f1(double d) { }
     int f2(double d) { }
};

typedef int (DelegateList::* DelegateType)(double d);

DelegateType d = &DelegateList::f1;
DelegateList list;
int a = (list.*d)(3.14);

选项 5: std::function

(或者如果您的标准库不支持它,可以使用boost::function)。它速度较慢,但是它是最灵活的。


#include <functional>
std::function<int(double)> f = [can be set to about anything in this answer]
// Usually more useful as a parameter to another functions

选项6:绑定(使用 std::bind

允许预先设置一些参数,方便调用成员函数。

struct MyClass
{
    int DoStuff(double d); // actually a DoStuff(MyClass* this, double d)
};

std::function<int(double d)> f = std::bind(&MyClass::DoStuff, this, std::placeholders::_1);
// auto f = std::bind(...); in C++11

选项7: 模板

只要与参数列表匹配,就接受任何东西。

template <class FunctionT>
int DoSomething(FunctionT func)
{
    return func(3.14);
}

2
很好的列表,+1。然而,在这里只有两个真正被视为代理——捕获lambda和从std::bind返回的对象,它们实际上做相同的事情,只是lambda不能像多态一样接受不同的参数类型。 - Xeo
1
@MatthieuM.:说得好。 我曾考虑过函数指针是遗留问题,但这可能只是我的个人喜好。 - J.N.
1
@Xeo:我对委托的理解是比较经验性的。也许我混淆了函数对象和委托(怪罪我的以前的C#经验)。 - J.N.
2
@SirYakalot 有些东西的行为类似于函数,但同时可能会保持状态并像任何其他变量一样被操作。一个用途是将需要两个参数的函数强制将第一个参数具有特定值创建一个新的只有一个参数(我的列表中的binding)的函数。你不能使用函数指针实现这一点。 - J.N.
4
在 C++ 中本身并不存在它们,因此需要使用列表。 - J.N.
显示剩余8条评论

43

委托是一个类,它将一个指针或引用包装到一个对象实例、该对象的类的成员方法要在该对象实例上调用,并提供一个触发调用的方法。

这里有一个例子:

template <class T>
class CCallback
{
public:
    typedef void (T::*fn)( int anArg );

    CCallback(T& trg, fn op)
        : m_rTarget(trg)
        , m_Operation(op)
    {
    }

    void Execute( int in )
    {
        (m_rTarget.*m_Operation)( in );
    }

private:

    CCallback();
    CCallback( const CCallback& );

    T& m_rTarget;
    fn m_Operation;

};

class A
{
public:
    virtual void Fn( int i )
    {
    }
};


int main( int /*argc*/, char * /*argv*/ )
{
    A a;
    CCallback<A> cbk( a, &A::Fn );
    cbk.Execute( 3 );
}

提供触发调用的方法是什么?委托?如何实现?函数指针? - Dollarslice
委托类将提供一个名为Execute()的方法,该方法触发委托所包装对象上的函数调用。 - Grimm The Opiner
5
建议您在这种情况下覆盖调用运算符(void CCallback::operator()(int)), 而不是执行函数。这是因为在泛型编程中,可调用对象应该像函数一样被调用。o.Execute(5)将不兼容,但o(5)将非常适合作为可调用模板参数。这个类也可以通用化,但我假设为了简短起见保持了它的简单性(这是一件好事)。除此之外,这是一个非常有用的类。 - David Peterson

23
需要C++委托实现是C++社区长期尴尬的问题。每个C++程序员都希望拥有它们,因此他们最终会使用它们,尽管事实如下:
  1. std::function() 使用堆操作(并且对于严肃的嵌入式编程来说无法使用)。

  2. 所有其他实现都在不同程度上向可移植性或标准一致性做出妥协(请通过检查此处和codeproject上的各种委托实现来验证)。我还没有看到一个不使用野生reinterpret_casts、嵌套类“原型”(希望产生与用户传入的函数指针大小相同的函数指针),编译器诡计(比如先前置声明,然后typedef再声明,这次从另一个类继承或类似的不良技巧)的实现。虽然对于构建它的实现者来说这是一个伟大的成就,但这仍然是C++进化方式的悲哀见证。

  3. 很少有人指出,现在已经超过三个C++标准修订版本,委托没有得到适当的解决。(或者缺乏语言功能,允许简单直接的委托实现。)

  4. 随着C++11 lambda函数按标准定义(每个lambda都有匿名的、不同类型),情况只在某些用例中得到改善。但对于在(DLL)库API中使用委托的用例来说,仅使用lambda函数仍然无法使用。这里常见的技巧是先将lambda打包到std::function中,然后通过API传递。


我已经基于其他地方看到的想法,使用C++11实现了Elbert Mai的通用回调版本,并且它似乎清除了你在2)中提到的大部分问题。 对我来说唯一仍然困扰的问题是我被困在客户端代码中使用宏来创建委托。可能有一种模板魔法方法可以解决这个问题,但我还没有找到。 - kert
5
我在嵌入式系统中使用不需要堆内存的std::function,它仍然能够正常工作。 - prasad
2
std::function 并不总是动态分配内存。 - Lightness Races in Orbit
9
这个回答更像是一场牢骚而非真正的回答。 - Lightness Races in Orbit

10

简单来说,委托提供了函数指针应该工作的功能。在 C++ 中,函数指针有很多限制。委托使用一些幕后模板技巧来创建一个类似于模板类函数指针类型的东西,以满足你可能想要的工作方式。

也就是说 - 你可以将它们设置为指向给定的函数,随时随地传递它们并调用它们。

这里有一些很好的例子:


5

在C++中,委托的一种选项是使用函数指针和上下文参数进行C风格的实现。这可能是许多提出此问题的人试图避免的模式。但是,该模式具有可移植性、高效性,并且可用于嵌入式和内核代码。

class SomeClass
{
    in someMember;
    int SomeFunc( int);

    static void EventFunc( void* this__, int a, int b, int c)
    {
        SomeClass* this_ = static_cast< SomeClass*>( this__);

        this_->SomeFunc( a );
        this_->someMember = b + c;
    }
};

void ScheduleEvent( void (*delegateFunc)( void*, int, int, int), void* delegateContext);

    ...
    SomeClass* someObject = new SomeObject();
    ...
    ScheduleEvent( SomeClass::EventFunc, someObject);
    ...

1

Windows Runtime 中等同于标准 C++ 中函数对象的概念。可以将整个函数作为参数(实际上是函数指针)。它通常与事件一起使用。委托代表了事件处理程序必须遵守的契约。它简化了函数指针的使用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接