Click here to Skip to main content
15,867,308 members
Articles / Programming Languages / Visual C++ 12.0

Variadic Delegates

Rate me:
Please Sign up or sign in to vote.
5.00/5 (11 votes)
13 Apr 2015CPOL8 min read 20.9K   268   22   4
This tutorial showcases how to implement C++ delegates which are capable of being bound to methods and functions having arbitrary signature, i.e., any number and type of parameters and return value.

Introduction

At some point, I was in need of writing a subsystem for my framework that creates graphics effects. One of the traits of many render effects is the execution of operations’ set, changing the pipeline state before the shader part comes into action and after it. In other words, we need kind of operator brackets – Begin()/End() functions – inside which effects’ algorithm will take the action. I didn’t find made-up solutions and wrote my own implementation.

It was needed to save function and method delegates side by side in one container. The signature of a delegated entity should be arbitrary. There must be the capability to bind delegated methods and functions with arguments before the actual call and thus – the capability to call right away the entire delegates’ collection with tied in advance arguments. The args should be passed and saved via perfect forwarding, in order not to make redundant copies. While making the delegate, user should not write superfluous code, the notation must be in terms with common sense, no signatures and data types in template parameters. That’s how the delegates are supposed to be used:

C#
// Constructing delegates' collection
DelegatesCollection dc;
// Adding delegates for functions f1, f2 and for methods meth1, meth2
dc.add(&f1);
dc.add(&obj1, &meth1);
dc.add(&obj2, &meth2);
dc.add(&f2);
// Calling f1 through its delegate and passing in arguments
dc[0](arg1, arg2, . . ., argN);
// Binding arguments to meth1's delegate
dc[1].bind_args(arg1, arg2, . . ., argN);
// Calling meth1 with previously bound arguments
dc[1].call_with_bound_args();
// Binding arguments to meth2 and f2
dc[2].bind_args(arg1, arg2, . . ., argN);
dc[3].bind_args(arg1, arg2, . . ., argN);
// Batch calling entire delegates' collection, 
// i.e. f1, meth1, meth2 and f2, with previously bound arguments
dc.batch_call_with_bound_args();

All in all, everything that had been put-up was implemented.

Features:

  1. Capability of storing function and method delegates together in one container
  2. Ability to batch call the entire delegates’ collection using bound args
  3. Delegates are stored in container std::vector
  4. Arguments and result data types are deduced automatically from function signature – there’s no need to specify them as template parameters

Delegates Design

Delegates are designed so that next tasks are solved:

  1. To store method delegates (MD) and function delegates (FD), both entities must be described with one interface.
  2. In the output, we should receive Delegate class comprising the implementation of the notion delegate.
  3. The user should have the possibility to create delegates’ collection. This will require DelegatesSystem class, which will be the container for Delegate instances.
  4. DelegatesSystem must provide the interface for batch call of delegated functions and methods with bound arguments, i.e., for sequential launch of all delegates’ collection elements.

Making the Interface for MD and FD

Since in general case, we’ll delegate methods and functions with arbitrary parameters count, the interface being made must be a template. For MD creation pointers to object and its method must be present, for FD – pointer to function. These pointers will be passed through template params.

Class Delegate must be independent of concrete data types, since  it is a generic abstraction of the delegate notion, thus it cannot be a template.

The question arises, in which way then we have to describe MD and FD with one interface and associate them with Delegate class?

The only way of doing it is to create an interface general for MD and FD and to store its instances, initialized with the appropriate pointers to implementations, inside Delegate.

The next question is how to get to template interface for MD and FD would have described these entities simultaneously?

Fortunately, there is such thing as partial template specialization, which we’ll use for solving this task.

Basically MD and FD are the same thing, they differ only in one component. Therefore, the interface describing them will have two specializations.

Implementation

Generic interface:

C++
class IDelegateData abstract { needed pure virtual methods are here };

Generic template:

C++
template<class...Args> class DelegateData : public IDelegateData {};

Since methods are distinct from functions in that former are called on a concrete object of a class, thus specialization of the generic template for MD will additionally have parameter for class of object to which the method belongs: class O.

So, we’ll get the following two specializations of generic template interface:

For Methods

C++
template<class R, class O, class... Args>
class DelegateData<R, O, R(Args...)> : public IDelegateData
{
public:

typedef R(O::*M)(Args...);

DelegateData(O* pObj, M pMethod) : m_pObj(pObj), m_pMethod(pMethod) {}
private:

    O* m_pObj;
    M  m_pMethod;
};

For Functions

C++
template<class R, class... Args>
class DelegateData<R, R(*)(Args...)> : public IDelegateData
{
public:

typedef R(*F)(Args...);

DelegateData(F pF) : m_pF(pF) {}
private:

    F m_pF;
};

Delegate

Storing instances of DelegateData inside Delegate:

C++
class Delegate
{
public:

Delegate() = default;

// For methods

template<class R, class O, class...Args>
explicit Delegate(O* pObj, R(O::*M)(Args...))
{
    bind(pObj, M);
}

template<class R, class O, class...Args>
void bind(O* pObj, R(O::*M)(Args...))
{
    m_data = new DelegateData<R, O, R(Args...)>(pObj, M);
}

// For functions
template<class R, class...Args>
explicit Delegate(R(*F)(Args...))
{
    bind(F);
}

template<class R, class...Args>
void bind(R(*F)(Args...))
{
    m_data = new DelegateData<R, R(*)(Args...)>(F);
}

private:

    IDelegateData* m_data;

};

Field Delegate::m_data will be filled with the specialization of DelegateData for MD or FD accordingly to which Delegate ctor is called.

DelegatesSystem

Let’s consider DelegatesSystem class, which will store delegates’ collection in the form of Delegate objects:

C++
class DelegatesSystem
{
private:
    vector<Delegate> m_delegates;
};

It’s left to “teach” DelegatesSystem to add delegates Delegate into the collection and to gain access to them by index.

The easiest way of adding delegates to the collection is to simply list the arguments needed for Delegate ctor in the addition method. For this, we’ll require variadic templates, rvalue references and new sequential containers’ method emplace_back(...), taking arguments pack and adding new element into the collection via constructing the object in place (in place construction), calling that ctor of its class which corresponds to the arguments passed.

C++
class DelegatesSystem
{
public:

template<class...Args>
void add(Args&&... delegateCtorArgs)
{
    m_delegates.emplace_back(std::forward<Args>(delegateCtorArgs)...);
}

Delegate& operator[](uint idx)
{
    return delegates[idx];
}

private:

    vector<Delegate> m_delegates;
};

Now, we can add delegates by calling any ctor of a Delegate class and all this is via one method.

Arguments

We are close to our goal. The only thing left is to provide for the possibility of calling delegates with an arbitrary number of arguments. Thus, you have to start with making changes in the interface, which is empty for the moment. Let’s add to IDelegateData pure virtual method for calling delegates:

C++
class IDelegateData abstract { public: virtual void call(void*) abstract; };

We’ll receive arguments inside the method DelegateData::call, passing them there being placed in the instance of Arguments class, from where we’ll extract them, casting void* to Arguments<...>*.

To the details. Arguments will be stored inside Arguments in tuple, since it’s a container, allowing to store arbitrary number of different type elements:

C++
template<class... Args>
class Arguments
{
public:

    Arguments(Args&&... args) : m_args(std::forward_as_tuple(std::forward<Args>(args)...)) {}

public:

    std::tuple<Args&&...> m_args;
};

Everywhere while passing the arguments, we use perfect forwarding in order to draw move semantics for rvalues and for lvalues to be passed by reference.

We have the following call chain leading to call of the method/function being delegated:

C++
Delegate::operator() -> DelegateData::call

We write overload of operator Delegate::operator(), using variadic templates, to make it capable of receiving any number of arguments:

C++
class Delegate
{
public:

previous declarations

template<class...Args>
void operator()(Args&&... args)
{
    m_data->call(new Arguments<Args...>(std::forward<Args>(args)...));
}

private:

    IDelegateData* m_data;
};

Here we create instance of Arguments, store args arguments pack in it and pass it to the method call() of a polymorphic field m_data, which triggers call() of a corresponding specialization of DelegateData, where arguments are somehow extracted and passed for invocation of a delegated function/method:

C++
template<class R, class O, class... Args>
class DelegateData<R, O, R(Args...)> : public IDelegateData
{
public:

previous declarations

void call(void* pArgs) override
{
   extraction of arguments from pArgs and passing them to the delegated function/method;
   i.e. in the high-level sense the following will occur:

(m_pObj->*m_pMethod)(extraction of arguments pack from pArgs);
}

private:

    O* m_pObj;
    M  m_pMethod;

};

For DelegateData specialization implementing FD conception everything looks similar.

Extraction of Arguments from Tuple and Passing Them to Delegate

Now arises the task of extracting arguments from tuple and passing them to method or function. At the moment, there is a standard way of extracting tuple’s elements one at a time, for what we have template function std::get<idx>(tuple) described in header <tuple>, which takes index of required element as its template parameter idx. What we have is parameter pack Args (defined in specializations of DelegateData), which contains data types of arguments of delegated function/method, thus we know how to cast from void*. We’d like to solve the given task as follows (we’ll use example of MD and without perfect forwarding):

C++
(m_pObj->*m_pMethod)(std::get<what_goes_here>(static_cast<Arguments<Args...>*>(pArgs)->args)...);

i.e., we need parameter pack Args for casting pArgs to the appropriate pointer to Arguments. Call of a function get(), should be done on account of indices pack expansion, what would allow to extract arguments stored in tuple as one list and to pass them to the delegated method/function. Therefore now we have to find a way of generating indices list corresponding to the number of params in Args pack. There is such a way, it’s underlain by metaprogramming and template recursion [IndicesTrick]. Here’s how indices creation mechanism looks like:

C++
template<int... Idcs> class Indices{};

template<int N, int... Idcs> struct IndicesBuilder : IndicesBuilder<N - 1, N - 1, Idcs...> {};

template<int... Idcs>
struct IndicesBuilder<0, Idcs...>
{
    typedef Indices<Idcs...> indices;
};

For the sake of speeding things up, I’ll give a brief high-level explanation of this technique’s operational principles for those, who isn’t familiar with metaprogramming and template recursion [Metaprog]. IndicesBuilder template should be considered as a function f(N), which takes integral argument, denoting the number of indices, and returns the pack of these indices. To calculate the number of indices, we’ll harness new function sizeof...(parameter_pack), applying it to Args:  sizeof...(Args).

Indices passing we’ll mediate with yet another one method where we’ll make call of the delegated method:

C++
template<int...Idcs>
void invoker(Indices<Idcs...>, void* pArgs)
{
    (m_pObj->*m_pMethod)(std::get<Idcs>(static_cast<Arguments<Args...>*>(pArgs)->args)...);
}

After adding perfect forwarding and simplifying the expression:

C++
template<int...Idcs>
void invoker(Indices<Idcs...>, void* pArgs)
{
    auto pArguments = static_cast<Arguments<Args...>*>(pArgs);
    (m_pObj->*m_pMethod)(std::get<Idcs>(pArguments->m_args)...);
}

The final implementation of the overridden method call will look the following way:

C++
void call(void* pArgs) override
{
    invoker(typename IndicesBuilder<sizeof...(Args)>::indices(), pArgs);
}

The specialization of DelegateData template for MD entirely:

C++
template<class R, class O, class... Args>
class DelegateData<R, O, R(Args...)> : public IDelegateData
{
public:

typedef R(O::*M)(Args...);

    DelegateData(O* pObj, M pMethod) : m_pObj(pObj), m_pMethod(pMethod) {}

void call(void* pArgs) override
{
    invoker(typename IndicesBuilder<sizeof...(Args)>::indices(), pArgs);
}

template<int...Idcs>
void invoker(Indices<Idcs...>, void* pArgs)
{
    auto pArguments = static_cast<Arguments<Args...>*>(pArgs);
    (m_pObj->*m_pMethod)(std::get<Idcs>(pArguments->m_args)...);
}

private:

    O* m_pObj;
    M  m_pMethod;
};

The specialization for FD will look similar.

So, the task of storing arbitrary number of function and method delegates with any given signature and making it possible to call them later is solved. It’s left to add arguments binding and batch call of the delegates.

Arguments Binding and Batch Call of the Delegates

For making delegates’ batch call possible, we’ll have to bind arguments to them. Respectively, each delegate should be able to store its argument set. And by the time of delegates’ batch call, arguments tied with them will be passed to the corresponding methods/functions.

IDelegateData

Let’s add interfaces of binding arguments to the delegate - bind_args and of calling the delegate with bound arguments - call_with_bound_args, and field void* m_pBound_args, where the very arguments will be stored:

C++
class IDelegateData abstract
{
public:
    virtual void call(void*) abstract;
    virtual void call_with_bound_args() abstract;
    virtual void bind_args(void*) abstract;
protected:
    void* m_pBound_args;
};

DelegateData

For storing arguments, let’s add in every specialization of DelegateData ctor overload, and override bind_args and call_with_bound_args:

C++
// Data for methods
template<class R, class O, class... Args>
class DelegateData<R, O, R(Args...)> : public IDelegateData
{
public:

typedef R(O::*M)(Args...);

DelegateData(O* pObj, M pMethod) : m_pObj(pObj), m_pMethod(pMethod) {}

void call(void* pArgs) override
{
    invoker(typename IndicesBuilder<sizeof...(Args)>::indices(), pArgs);
}

template<int...Idcs>
void invoker(Indices<Idcs...>, void* pArgs)
{
    auto pArguments = static_cast<Arguments<Args...>*>(pArgs);
    (m_pObj->*m_pMethod)(std::get<Idcs>(pArguments->m_args)...);
}

public:

DelegateData(O* pObj, M pMethod, Args&&... argsToBind) : m_pObj(pObj), m_pMethod(pMethod)
{
    bind_args(new Arguments<Args&&...>(std::forward<Args>(argsToBind)...));
}

virtual void bind_args(void* argsToBind) override
{
    if (argsToBind != m_pBound_args)
    {
        delete m_pBound_args;
        m_pBound_args = argsToBind;
    }
}

void call_with_bound_args() override
{
    invoker(typename IndicesBuilder<sizeof...(Args)>::indices(), m_pBound_args);
}

private:

    O* m_pObj;
    M  m_pMethod;
};

// Data for functions
template<class R, class... Args>
class DelegateData<R, R(*)(Args...)> : public IDelegateData
{
public:

typedef R(*F)(Args...);

DelegateData(F pF) : m_pF(pF) {}

void call(void* pArgs) override
{
    invoker(typename IndicesBuilder<sizeof...(Args)>::indices(), pArgs);
}

template<int...Idcs>
void invoker(Indices<Idcs...>, void* pArgs)
{
    auto pArguments = static_cast<Arguments<Args...>*>(pArgs);
    m_pF(std::get<Idcs>(pArguments->m_args)...);
}

public:

DelegateData(F pF, Args&&... argsToBind) : m_pF(pF)
{
    bind_args(new Arguments<Args&&...>(std::forward<Args>(argsToBind)...));
}

virtual void bind_args(void* argsToBind) override
{
    if (argsToBind != m_pBound_args)
    {
        delete m_pBound_args;
        m_pBound_args = argsToBind;
    }
}

void call_with_bound_args() override
{
    invoker(typename IndicesBuilder<sizeof...(Args)>::indices(), m_pBound_args);

}

private:

    F m_pF;
};

Delegate

To the Delegate, we’ll add overloads of ctors and methods: bind, template method bind_args and call_with_bound_args, which will serve merely as wrappers over interfaces of IDelegateData:

C++
class Delegate
{
public:
...

template<class R, class O, class...Args, class...ArgsToBind>
explicit Delegate(O* m_pObj, R(O::*M)(Args...), ArgsToBind&&... argsToBind)
{
    bind(m_pObj, M, std::forward<ArgsToBind>(argsToBind)...);
}
template<class R, class...Args, class...ArgsToBind>
explicit Delegate(R(*F)(Args...), ArgsToBind&&... argsToBind)
{
    bind(F, std::forward<ArgsToBind>(argsToBind)...);
}
template<class R, class O, class...Args, class...ArgsToBind>
void bind(O* pObj, R(O::*M)(Args...), ArgsToBind&&... argsToBind)
{
    m_data = new DelegateData<R, O, R(Args...)>(pObj, M, std::forward<ArgsToBind>(argsToBind)...);
}
template<class R, class...Args, class...ArgsToBind>
void bind(R(*F)(Args...), ArgsToBind&&... args)
{
    m_data = new DelegateData<R, R(*)(Args...)>(F, std::forward<ArgsToBind>(args)...);
}
template<class... Args>
void bind_args(Args&&... args)
{
    m_data->bind_args(new Arguments<Args...>(std::forward<Args>(args)...));
}

void call_with_bound_args()
{
    m_data->call_with_bound_args();
}
...
};

DelegatesSystem

To the DelegatesSystem, we’ll add launch for a batch call of entire delegates’ collection:

C++
class DelegatesSystem
{
public:
...
void launch()
{
    for (auto& d : m_delegates)
        d.call_with_bound_args();
}
private:
    vector<Delegate> m_delegates;
};

Sample Code

The accompanying code for this tutorial was written using Visual Studio 2013 Community Edition.

References

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Russian Federation Russian Federation
Working/ed as Programmer at: Ubisoft, 1С Warsaw (Bandai Namco projects)

Projects worked on:
Far Cry 4,
Far Cry Primal,
Steep,
Ni No Kuni

Comments and Discussions

 
QuestionPossible improvements? Pin
Michaelgor25-May-17 6:31
Michaelgor25-May-17 6:31 
Hi,

1. You create a new Arguments instance on the heap for each call. If I didn´t miss something, it´s a mem leak, no one deletes it. Also, its unnecessary slow.

2. It´s not typesafe. There are no checks at all, if you call a delegate with wrong argument types (see also Michał Gębołyś comment). I did something similar once, and I did a dynamic_cast instead of static_cast, and I returned a boost::optional to indicate, if the call was done. But I never realy liked my solution too. There should be some more clever template tricks to check if the passed arguments match the required arguments.
QuestionProblem with c style (const char*) strings Pin
Michał Gębołyś13-May-16 0:52
Michał Gębołyś13-May-16 0:52 
QuestionBest C++ Article of April 2015 Pin
Sibeesh Passion8-May-15 4:55
professionalSibeesh Passion8-May-15 4:55 
AnswerRe: Best C++ Article of April 2015 Pin
Anton Chibisov14-May-15 8:13
Anton Chibisov14-May-15 8:13 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.