5 年后,还有什么比“Fastest possible C++ Delegates"更好的吗?

2022-01-18 00:00:00 performance delegates c++

我知道C++代表"的话题已经被做死了,而且两个http://www.codeproject.com 和 http://stackoverflow.com 深入探讨了这个问题.

I know that the topic of "C++ delegates" has been done to death, and both http://www.codeproject.com and http://stackoverflow.com deeply cover the question.

一般来说,Don Clugston 最快的代理似乎是首选对许多人来说.还有一些其他受欢迎的.

Generally, it seems that Don Clugston's fastest possible delegate is the first choice for many people. There are a few other popular ones.

但是,我注意到这些文章中的大多数都是旧的(大约在 2005 年左右),而且许多设计选择似乎都是考虑到像 VC7 这样的旧编译器.

However, I noticed that most of those articles are old (around 2005) and many design choices seem to have been made taking in account old compilers like VC7.

我需要一个非常快速的音频应用程序委托实现.

I'm in need of a very fast delegate implementation for an audio application.

我仍然需要它是可移植的(Windows、Mac、Linux),但我只使用现代编译器(VC9,VS2008 SP1 和 GCC 4.5.x 中的编译器).

I still need it to be portable (Windows, Mac, Linux) but I only use modern compilers (VC9, the one in VS2008 SP1 and GCC 4.5.x).

我的主要标准是:

  • 一定要快!
  • 它必须与较新版本的编译器前向兼容.我对 Don 的实现有些怀疑,因为他明确表示它不符合标准.
  • 可选地,KISS 语法和易用性是不错的选择
  • 多播会很好,尽管我相信围绕任何委托库构建它真的很容易

此外,我真的不需要异国情调的功能.我只需要好的旧指针方法的东西.不需要支持静态方法、自由函数或类似的东西.

Furthermore, I don't really need exotic features. I just need the good old pointer-to-method thing. No need to support static methods, free functions or things like that.

截至今天,推荐的方法是什么?还在使用 Don 的版本?或者是否有关于另一种选择的社区共识"?

As of today, what is the recommended approach? Still use Don's version? Or is there a "community consensus" about another option?

我真的不想使用 Boost.signal/signal2,因为它在性能方面是不可接受的.对 QT 的依赖也是不可接受的.

I really don't want to use Boost.signal/signal2 because it's not acceptable in terms of performance. A dependency on QT is not acceptable as well.

此外,我在谷歌搜索时看到了一些较新的库,例如 cpp-events 但我找不到用户的任何反馈,包括关于 SO.

Furthermore, I've seen some newer libraries while googling, like for example cpp-events but I couldn't find any feedback from users, including on SO.

推荐答案

更新: 一篇包含完整源代码的文章和更详细的讨论已发布在 The Code Project 上.

嗯,指向方法的指针的问题在于它们的大小并不完全相同.因此,我们不需要直接存储指向方法的指针,而是需要标准化"它们,使它们具有恒定的大小.这就是 Don Clugston 在他的 Code Project 文章中试图实现的目标.他使用最流行的编译器的深入知识来做到这一点.我断言可以在普通"C++ 中做到这一点,而不需要这些知识.

Well, the problem with pointers to methods is that they're not all the same size. So instead of storing pointers to methods directly, we need to "standardize" them so that they are of a constant size. This is what Don Clugston attempts to achieve in his Code Project article. He does so using intimate knowledge of the most popular compilers. I assert that it's possible to do it in "normal" C++ without requiring such knowledge.

考虑以下代码:

void DoSomething(int)
{
}

void InvokeCallback(void (*callback)(int))
{
    callback(42);
}

int main()
{
    InvokeCallback(&DoSomething);
    return 0;
}

这是使用普通旧函数指针实现回调的一种方法.但是,这不适用于对象中的方法.让我们解决这个问题:

This is one way to implement a callback using a plain old function pointer. However, this doesn't work for methods in objects. Let's fix this:

class Foo
{
public:
    void DoSomething(int) {}

    static void DoSomethingWrapper(void* obj, int param)
    {
        static_cast<Foo*>(obj)->DoSomething(param);
    }
};

void InvokeCallback(void* instance, void (*callback)(void*, int))
{
    callback(instance, 42);
}

int main()
{
    Foo f;
    InvokeCallback(static_cast<void*>(&f), &Foo::DoSomethingWrapper);
    return 0;
}

现在,我们有一个回调系统,可以同时用于自由函数和成员函数.然而,这很笨拙且容易出错.但是,有一种模式 - 使用包装函数将静态函数调用转发"到正确实例上的方法调用.我们可以使用模板来帮助解决这个问题――让我们尝试泛化包装函数:

Now, we have a system of callbacks that can work for both free and member functions. This, however, is clumsy and error-prone. However, there is a pattern - the use of a wrapper function to "forward" the static function call to a method call on the proper instance. We can use templates to help with this - let's try generalizing the wrapper function:

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

}

class Foo
{
public:
    void DoSomething(int) {}
};

void InvokeCallback(void* instance, void (*callback)(void*, int))
{
    callback(instance, 42);
}

int main()
{
    Foo f;
    InvokeCallback(static_cast<void*>(&f),
        &Wrapper<void, Foo, int, &Foo::DoSomething> );
    return 0;
}

这仍然非常笨拙,但至少现在我们不必每次都写出包装函数(至少对于 1 参数的情况).我们可以概括的另一件事是我们总是传递一个指向 void* 的指针.与其将其作为两个不同的值传递,不如将它们放在一起?当我们这样做的时候,为什么不把它概括一下呢?嘿,让我们加入一个 operator()(),这样我们就可以像函数一样调用它了!

This is still extremely clumsy, but at least now we don't have to write out a wrapper function every single time (at least for the 1 argument case). Another thing we can generalize is the fact that we're always passing a pointer to void*. Instead of passing it as two different values, why not put them together? And while we're doing that, why not generalize it as well? Hey, let's throw in an operator()() so we can call it like a function!

template<typename R, typename A1>
class Callback
{
public:
    typedef R (*FuncType)(void*, A1);

    Callback(void* o, FuncType f) : obj(o), func(f) {}
    R operator()(A1 a1) const
    {
        return (*func)(obj, a1);
    }

private:
    void* obj;
    FuncType func;
};

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

}

class Foo
{
public:
    void DoSomething(int) {}
};

void InvokeCallback(Callback<void, int> callback)
{
    callback(42);
}

int main()
{
    Foo f;
    Callback<void, int> cb(static_cast<void*>(&f),
        &Wrapper<void, Foo, int, &Foo::DoSomething>);
    InvokeCallback(cb);
    return 0;
}

我们正在取得进展!但现在我们的问题是语法绝对可怕.语法显得多余;编译器不能从指向方法本身的指针中找出类型吗?不幸的是,没有,但我们可以帮助它.请记住,编译器可以通过函数调用中的模板参数推导来推导类型.那我们为什么不从那开始呢?

We're making progress! But now our problem is the fact that the syntax is absolutely horrible. The syntax appears redundant; can't the compiler figure out the types from the pointer to method itself? Unfortunately no, but we can help it along. Remember that a compiler can deduce types via template argument deduction in a function call. So why don't we start with that?

template<typename R, class T, typename A1>
void DeduceMemCallback(R (T::*)(A1)) {}

在函数内部,我们知道RTA1是什么.那么,如果我们可以构造一个可以保存"这些类型并从函数中返回它们的结构呢?

Inside the function, we know what R, T and A1 is. So what if we can construct a struct that can "hold" these types and return them from the function?

template<typename R, class T, typename A1>
struct DeduceMemCallbackTag
{
};

template<typename R, class T, typename A1>
DeduceMemCallbackTag2<R, T, A1> DeduceMemCallback(R (T::*)(A1))
{
    return DeduceMemCallbackTag<R, T, A1>();
}

既然 DeduceMemCallbackTag 知道类型,为什么不把我们的包装函数作为一个静态函数放在里面呢?而且既然包装函数在里面,为什么不把构造我们的Callback对象的代码放在里面呢?

And since DeduceMemCallbackTag knows about the types, why not put our wrapper function as a static function in it? And since the wrapper function is in it, why not put the code to construct our Callback object in it?

template<typename R, typename A1>
class Callback
{
public:
    typedef R (*FuncType)(void*, A1);

    Callback(void* o, FuncType f) : obj(o), func(f) {}
    R operator()(A1 a1) const
    {
        return (*func)(obj, a1);
    }

private:
    void* obj;
    FuncType func;
};

template<typename R, class T, typename A1>
struct DeduceMemCallbackTag
{
    template<R (T::*Func)(A1)>
    static R Wrapper(void* o, A1 a1)
    {
        return (static_cast<T*>(o)->*Func)(a1);
    }

    template<R (T::*Func)(A1)>
    inline static Callback<R, A1> Bind(T* o)
    {
        return Callback<R, A1>(o, &DeduceMemCallbackTag::Wrapper<Func>);
    }
};

template<typename R, class T, typename A1>
DeduceMemCallbackTag<R, T, A1> DeduceMemCallback(R (T::*)(A1))
{
    return DeduceMemCallbackTag<R, T, A1>();
}

C++ 标准允许我们在实例上调用静态函数 (!):

The C++ standard allows us to call static functions on instances (!):

class Foo
{
public:
    void DoSomething(int) {}
};

void InvokeCallback(Callback<void, int> callback)
{
    callback(42);
}

int main()
{
    Foo f;
    InvokeCallback(
        DeduceMemCallback(&Foo::DoSomething)
        .Bind<&Foo::DoSomething>(&f)
    );
    return 0;
}

尽管如此,这是一个冗长的表达式,但我们可以将其放入一个简单的宏 (!) 中:

Still, it's a lengthy expression, but we can put that into a simple macro (!):

template<typename R, typename A1>
class Callback
{
public:
    typedef R (*FuncType)(void*, A1);

    Callback(void* o, FuncType f) : obj(o), func(f) {}
    R operator()(A1 a1) const
    {
        return (*func)(obj, a1);
    }

private:
    void* obj;
    FuncType func;
};

template<typename R, class T, typename A1>
struct DeduceMemCallbackTag
{
    template<R (T::*Func)(A1)>
    static R Wrapper(void* o, A1 a1)
    {
        return (static_cast<T*>(o)->*Func)(a1);
    }

    template<R (T::*Func)(A1)>
    inline static Callback<R, A1> Bind(T* o)
    {
        return Callback<R, A1>(o, &DeduceMemCallbackTag::Wrapper<Func>);
    }
};

template<typename R, class T, typename A1>
DeduceMemCallbackTag<R, T, A1> DeduceMemCallback(R (T::*)(A1))
{
    return DeduceMemCallbackTag<R, T, A1>();
}

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

class Foo
{
public:
    void DoSomething(int) {}
};

void InvokeCallback(Callback<void, int> callback)
{
    callback(42);
}

int main()
{
    Foo f;
    InvokeCallback(BIND_MEM_CB(&Foo::DoSomething, &f));
    return 0;
}

我们可以通过添加安全布尔值来增强 Callback 对象.禁用相等运算符也是一个好主意,因为无法比较两个 Callback 对象.更好的是,使用部分特化来允许首选语法".这给了我们:

We can enhance the Callback object by adding a safe bool. It's also a good idea to disable the equality operators since it's not possible to compare two Callback objects. Even better, is to use partial specialization to allow for a "preferred syntax". This gives us:

template<typename FuncSignature>
class Callback;

template<typename R, typename A1>
class Callback<R (A1)>
{
public:
    typedef R (*FuncType)(void*, A1);

    Callback() : obj(0), func(0) {}
    Callback(void* o, FuncType f) : obj(o), func(f) {}

    R operator()(A1 a1) const
    {
        return (*func)(obj, a1);
    }

    typedef void* Callback::*SafeBoolType;
    operator SafeBoolType() const
    {
        return func != 0? &Callback::obj : 0;
    }

    bool operator!() const
    {
        return func == 0;
    }

private:
    void* obj;
    FuncType func;
};

template<typename R, typename A1> // Undefined on purpose
void operator==(const Callback<R (A1)>&, const Callback<R (A1)>&);
template<typename R, typename A1>
void operator!=(const Callback<R (A1)>&, const Callback<R (A1)>&);

template<typename R, class T, typename A1>
struct DeduceMemCallbackTag
{
    template<R (T::*Func)(A1)>
    static R Wrapper(void* o, A1 a1)
    {
        return (static_cast<T*>(o)->*Func)(a1);
    }

    template<R (T::*Func)(A1)>
    inline static Callback<R (A1)> Bind(T* o)
    {
        return Callback<R (A1)>(o, &DeduceMemCallbackTag::Wrapper<Func>);
    }
};

template<typename R, class T, typename A1>
DeduceMemCallbackTag<R, T, A1> DeduceMemCallback(R (T::*)(A1))
{
    return DeduceMemCallbackTag<R, T, A1>();
}

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

使用示例:

class Foo
{
public:
    float DoSomething(int n) { return n / 100.0f; }
};

float InvokeCallback(int n, Callback<float (int)> callback)
{
    if(callback) { return callback(n); }
    return 0.0f;
}

int main()
{
    Foo f;
    float result = InvokeCallback(97, BIND_MEM_CB(&Foo::DoSomething, &f));
    // result == 0.97
    return 0;
}

我已经在 Visual C++ 编译器(版本 15.00.30729.01,VS 2008 附带的那个)上对此进行了测试,您确实需要一个相当新的编译器才能使用该代码.通过检查反汇编,编译器能够优化包装函数和 DeduceMemCallback 调用,减少到简单的指针分配.

I have tested this on the Visual C++ compiler (version 15.00.30729.01, the one that comes with VS 2008), and you do need a rather recent compiler to use the code. By inspection of the disassembly, the compiler was able to optimize away the wrapper function and the DeduceMemCallback call, reducing down to simple pointer assignments.

回调的双方都使用它很简单,并且只使用(我认为是)标准 C++.我上面显示的代码适用于具有 1 个参数的成员函数,但可以推广到更多参数.它还可以通过支持静态函数来进一步推广.

It's simple to use for both sides of the callback, and uses only (what I believe to be) standard C++. The code I've shown above works for member functions with 1 argument, but can be generalized to more arguments. It can also be further generalized by allowing support for static functions.

请注意,Callback 对象不需要堆分配 - 由于这种标准化"过程,它们具有恒定大小.因此,Callback 对象可以成为较大类的成员,因为它具有默认构造函数.它也是可赋值的(编译器生成的复制赋值函数就足够了).由于模板,它也是类型安全的.

Note that the Callback object requires no heap allocation - they are of a constant size thanks to this "standardization" procedure. Because of this, it's possible to have a Callback object be a member of larger class, since it has a default constructor. It is also assignable (the compiler generated copy assignment functions are sufficient). It is also typesafe, thanks to the templates.

相关文章