5 лет спустя, есть ли что-то лучшее, чем "самые быстрые возможные делегаты на C++"?

Я знаю, что тема "делегатов С++" была доведена до смерти, и http://www.codeproject.com и /qaru.site/... глубоко охватывают вопрос.

Как правило, кажется, что Дон Клугстон - самый быстрый возможный делегат - это первый выбор для многих людей. Есть еще несколько популярных.

Тем не менее, я заметил, что большинство из этих статей являются старыми (около 2005 года), и многие варианты дизайна, похоже, были сделаны с учетом старых компиляторов, таких как VC7.

Мне нужна очень быстрая реализация делегата для аудио приложения.

Мне все же нужно, чтобы он был переносимым (Windows, Mac, Linux), но я использую только современные компиляторы (VC9, тот, что в VS2008 SP1 и GCC 4.5.x).

Мои основные критерии:

  • он должен быть быстрым!
  • он должен быть совместим с новыми версиями компиляторов. У меня есть некоторые сомнения по поводу реализации Don, потому что он явно заявляет, что он не соответствует стандарту.
  • Необязательно, KISS-синтаксис и простота использования приятно иметь
  • многоадресная рассылка будет приятной, хотя я убежден, что ее очень легко создать в любой библиотеке делегатов.

Кроме того, мне действительно не нужны экзотические функции. Мне просто нужна хорошая старая вещь с указателем на метод. Нет необходимости поддерживать статические методы, бесплатные функции или что-то в этом роде.

На сегодняшний день, каков рекомендуемый подход? Все еще используйте Don version? Или существует "консенсус сообщества" по поводу другого варианта?

Я действительно не хочу использовать Boost.signal/signal2, потому что это не приемлемо с точки зрения производительности. Зависимость от QT также неприемлема.

Кроме того, я видел несколько новых библиотек во время поиска в googling, например cpp-events, но я не мог найти отзывов от пользователей, в том числе на SO.

Ответ 1

Обновление: Статья с полным исходным кодом и более подробное обсуждение опубликованы в проекте Code.

Ну, проблема с указателями на методы заключается в том, что они не все одинакового размера. Поэтому вместо того, чтобы напрямую хранить указатели на методы, нам нужно "стандартизировать" их, чтобы они имели постоянный размер. Это то, что пытается сделать Дон Клугстон в своей статье "Проект кодекса". Он делает это, используя интимные знания самых популярных компиляторов. Я утверждаю, что это возможно сделать в "нормальном" С++, не требуя таких знаний.

Рассмотрим следующий код:

void DoSomething(int)
{
}

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

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

Это один из способов реализации обратного вызова с использованием простого старого указателя функций. Однако это не работает для методов в объектах. Пусть исправить это:

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;
}

Теперь у нас есть система обратных вызовов, которая может работать как для бесплатных, так и для функций-членов. Это, однако, неудобно и подвержено ошибкам. Однако существует шаблон - использование функции-обертки для "пересылки" вызова статической функции вызову метода в соответствующем экземпляре. Мы можем использовать шаблоны, чтобы помочь с этим - попробуйте обобщить функцию обертки:

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()(), чтобы мы могли назвать его как функцию!

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;
}

Мы добиваемся прогресса! Но теперь наша проблема заключается в том, что синтаксис абсолютно ужасен. Синтаксис кажется излишним; не может ли компилятор определить типы из указателя на сам метод? К сожалению, нет, но мы можем помочь ему. Помните, что компилятор может выводить типы через вывод аргумента шаблона в вызове функции. Так почему бы нам не начать с этого?

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

Внутри функции мы знаем, что такое R, T и A1. Итак, что, если мы можем построить структуру, которая может "удерживать" эти типы и возвращать их из функции?

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 в нем?

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>();
}

Стандарт С++ позволяет называть статические функции на экземплярах (!):

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;
}

Тем не менее, это длинное выражение, но мы можем положить это в простой макрос (!):

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, добавив безопасный bool. Также неплохо отключить операторы равенства, так как невозможно сравнивать два объекта Callback. Еще лучше, это использовать частичную специализацию, чтобы разрешить "предпочтительный синтаксис". Это дает нам:

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 С++ (версия 15.00.30729.01, тот, который поставляется с VS 2008), и вам нужен довольно недавний компилятор для использования кода. При проверке разборки компилятор смог оптимизировать функцию обертки и вызов DeduceMemCallback, сократив до простых назначений указателей.

Он прост в использовании для обеих сторон обратного вызова и использует только (что я считаю) стандартным С++. Код, показанный выше, работает для функций-членов с 1 аргументом, но может быть обобщен на большее количество аргументов. Его также можно обобщить, разрешив поддержку статических функций.

Обратите внимание, что объект Callback не требует выделения кучи - они имеют постоянный размер благодаря этой процедуре "стандартизации". Из-за этого возможно, что объект Callback будет членом более крупного класса, так как он имеет конструктор по умолчанию. Он также можно присваивать (достаточны функции генерации копии, созданные компилятором). Это также типично, благодаря шаблонам.

Ответ 2

Я хотел отследить ответ @Insilico с небольшим количеством моих собственных материалов.

Прежде чем я наткнулся на этот ответ, я также пытался найти быстрые обратные вызовы, которые не наносили накладных расходов и были однозначно сопоставимы/идентифицированы только по сигнатуре функций. Что я создал - с некоторой серьезной помощью от клингонов, которые случались на барбекю - работает для всех типов функций (кроме Lambdas, если вы не храните Lambda, но не пытайтесь, потому что это действительно сложно и сложно сделать, и может привести к тому, что робот докажет вам, насколько это сложно и заставляет вас есть дерьмо для него). Благодаря @sehe, @nixeagle, @StackedCrooked, @CatPlusPlus, @Xeo, @DeadMG и, конечно, @Insilico за помощь в создании системы событий. Не стесняйтесь использовать, как хотите.

Во всяком случае, пример основан на ideone, но исходный код также здесь для вашего использования (потому что, поскольку Liveworkspace опустился, я не верю им в теневые компиляции сервисов. Кто знает, когда идеон спустится?!), Надеюсь, что это полезно для тех, кто не занят Лямбдой/Функцией, противопоставляя мир целям:

ВАЖНОЕ ПРИМЕЧАНИЕ: На данный момент (28/11/2012, 9:35 PM) Эта вариационная версия не будет работать с Microsoft VС++ 2012 Ноябрь CTP (Милан). Если вы хотите использовать его с этим, вам придется избавиться от всего вариационного материала и явно перечислить количество аргументов (и, возможно, шаблон-специализировать тип 1-аргумента для Event для void), чтобы сделать это Работа. Это боль, и до того, как я устал (и решил, что прохождение более 4-х аргументов было немного растянуто) мне удалось написать только 4 аргумента.

Пример источника

Источник:

#include <iostream>
#include <vector>
#include <utility>
#include <algorithm>

template<typename TFuncSignature>
class Callback;

template<typename R, typename... Args>
class Callback<R(Args...)> {
public:
        typedef R(*TFunc)(void*, Args...);

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

        R operator()(Args... a) const {
                return (*func)(obj, std::forward<Args>(a)...);
        }
        typedef void* Callback::*SafeBoolType;
        operator SafeBoolType() const {
                return func? &Callback::obj : 0;
        }
        bool operator!() const {
                return func == 0;
        }
        bool operator== (const Callback<R (Args...)>& right) const {
                return obj == right.obj && func == right.func;
        }
        bool operator!= (const Callback<R (Args...)>& right) const {
                return obj != right.obj || func != right.func;
        }
private:
        void* obj;
        TFunc func;
};

namespace detail {
        template<typename R, class T, typename... Args>
        struct DeduceConstMemCallback { 
                template<R(T::*Func)(Args...) const> inline static Callback<R(Args...)> Bind(T* o) {
                        struct _ { static R wrapper(void* o, Args... a) { return (static_cast<T*>(o)->*Func)(std::forward<Args>(a)...); } };
                        return Callback<R(Args...)>(o, (R(*)(void*, Args...)) _::wrapper);
                }
        };

        template<typename R, class T, typename... Args>
    struct DeduceMemCallback { 
                template<R(T::*Func)(Args...)> inline static Callback<R(Args...)> Bind(T* o) {
                        struct _ { static R wrapper(void* o, Args... a) { return (static_cast<T*>(o)->*Func)(std::forward<Args>(a)...); } };
                        return Callback<R(Args...)>(o, (R(*)(void*, Args...)) _::wrapper);
                }
        };

        template<typename R, typename... Args>
        struct DeduceStaticCallback { 
                template<R(*Func)(Args...)> inline static Callback<R(Args...)> Bind() { 
                        struct _ { static R wrapper(void*, Args... a) { return (*Func)(std::forward<Args>(a)...); } };
                        return Callback<R(Args...)>(0, (R(*)(void*, Args...)) _::wrapper); 
                }
        };
}

template<typename R, class T, typename... Args>
detail::DeduceConstMemCallback<R, T, Args...> DeduceCallback(R(T::*)(Args...) const) {
    return detail::DeduceConstMemCallback<R, T, Args...>();
}

template<typename R, class T, typename... Args>
detail::DeduceMemCallback<R, T, Args...> DeduceCallback(R(T::*)(Args...)) {
        return detail::DeduceMemCallback<R, T, Args...>();
}

template<typename R, typename... Args>
detail::DeduceStaticCallback<R, Args...> DeduceCallback(R(*)(Args...)) {
        return detail::DeduceStaticCallback<R, Args...>();
}

template <typename... T1> class Event {
public:
        typedef void(*TSignature)(T1...);
        typedef Callback<void(T1...)> TCallback;
        typedef std::vector<TCallback> InvocationTable;

protected:
        InvocationTable invocations;

public:
        const static int ExpectedFunctorCount = 2;

        Event() : invocations() {
                invocations.reserve(ExpectedFunctorCount);
        }

        template <void (* TFunc)(T1...)> void Add() {
                TCallback c = DeduceCallback(TFunc).template Bind<TFunc>();
                invocations.push_back(c);
        }

        template <typename T, void (T::* TFunc)(T1...)> void Add(T& object) {
                Add<T, TFunc>(&object);
        }

        template <typename T, void (T::* TFunc)(T1...)> void Add(T* object) {
                TCallback c = DeduceCallback(TFunc).template Bind<TFunc>(object);
                invocations.push_back(c);
        }

        template <typename T, void (T::* TFunc)(T1...) const> void Add(T& object) {
                Add<T, TFunc>(&object);
        }

        template <typename T, void (T::* TFunc)(T1...) const> void Add(T* object) {
                TCallback c = DeduceCallback(TFunc).template Bind<TFunc>(object);
                invocations.push_back(c);
        }

        void Invoke(T1... t1) {
                for(size_t i = 0; i < invocations.size() ; ++i) invocations[i](std::forward<T1>(t1)...); 
        }

        void operator()(T1... t1) {
                Invoke(std::forward<T1>(t1)...);
        }

        size_t InvocationCount() { return invocations.size(); }

        template <void (* TFunc)(T1...)> bool Remove ()          
        { return Remove (DeduceCallback(TFunc).template Bind<TFunc>()); } 
        template <typename T, void (T::* TFunc)(T1...)> bool Remove (T& object) 
        { return Remove <T, TFunc>(&object); } 
        template <typename T, void (T::* TFunc)(T1...)> bool Remove (T* object) 
        { return Remove (DeduceCallback(TFunc).template Bind<TFunc>(object)); } 
        template <typename T, void (T::* TFunc)(T1...) const> bool Remove (T& object) 
        { return Remove <T, TFunc>(&object); } 
        template <typename T, void (T::* TFunc)(T1...) const> bool Remove (T* object) 
        { return Remove (DeduceCallback(TFunc).template Bind<TFunc>(object)); } 

protected:
        bool Remove( TCallback const& target ) {
                auto it = std::find(invocations.begin(), invocations.end(), target);
                if (it == invocations.end()) 
                        return false;
                invocations.erase(it);
                return true;
        }
};