Search code examples
c++c++11templatescallbacktemplate-meta-programming

C++: use void as template argument


I have this minimal class to represent an event which client can subscribe to.
The event can have an data type associated to it, so when it is triggered by a publisher, an argument of that type would be passed to the client's callback:

template<typename Arg, typename Callback = function<void(const Arg&)>>
class Event
{
public:
    Event(Callback c) : mCallback(c){}
    
    void Trigger(const Arg& arg) {
        mCallback(arg);
    }

private:
    Callback mCallback;
};

Now I can create an Event<int> or any other concrete type, but it is really important to me to also allow "empty" event, which has no data associated with it: Event<void>

But sadly that doesn't work:

static void FooVoid() {
    cout << "Look ma, no args!" << endl;
}

static void FooInt(int a) {
    cout << "int arg " << a << endl;
}

int main()
{
    /* Compiles */
    Event<int> eInt(&FooInt);
    eInt.Trigger(42);

    /* Does not compile :(
    Event<void> eVoid(&FooVoid);
    eVoid.Trigger();
    */
    return 0;
}

Is there any way to achieve this desired API? How?

(P.S the solution should work on C++11)


Solution

  • The quickest way of solving this without explicitly specializing for void is to use a parameter pack (added in C++11) for your template argument instead of a single type and using an empty parameter pack instead of void. A parameter pack can homogeneously hold any number of type, including 0 and 1. Then it can be used to generate the right types and member functions. You basically just have to add ... correctly near every use of Arg (link) :

    #include <functional>
    #include <iostream>
    
    template<typename ... Arg>
    class Event
    {
    public:
        using Callback = std::function<void(const Arg&...)>;
    
        Event(Callback c) : mCallback(c){}
        
        void Trigger(const Arg& ... arg) {
            mCallback(arg...);
        }
    
    private:
        Callback mCallback;
    };
    
    static void FooVoid() {
        std::cout << "Look ma, no args!" << std::endl;
    }
    
    static void FooInt(int a) {
        std::cout << "int arg " << a << std::endl;
    }
    
    int main()
    {
        /* Compiles */
        Event<int> eInt(&FooInt);
        eInt.Trigger(42);
    
        Event<> eVoid(&FooVoid);
        eVoid.Trigger();
        
        return 0;
    }
    

    This has the added benefit that you can use callbacks with more than one argument. If this isn't desirable you can add a static_assert to prevent it :

    template<typename ... Arg>
    class Event
    {
    public:
        using Callback = std::function<void(const Arg&...)>;
        static_assert(sizeof...(Arg) <= 1, "Too many arguments");
    
        Event(Callback c) : mCallback(c){}
        
        void Trigger(const Arg& ... arg) {
            mCallback(arg...);
        }
    
    private:
        Callback mCallback;
    };
    

    Notice that this solution requires Event<> instead of Event<void>. You can solve that by adding a short specialization for Event<void> that uses Event<> (link) :

    template<>
    class Event<void> : public Event<>
    {
        // Inherit constructors
        using Event<>::Event;
    };