Click here to Skip to main content
15,867,330 members
Articles / Programming Languages / C++11

Implementation of Delegates in C++11

Rate me:
Please Sign up or sign in to vote.
4.93/5 (20 votes)
24 May 2013CPOL4 min read 74.5K   756   59   21
This is an alternative for "Member Function Pointers and the Fastest Possible C++ Delegates"

Introduction

In C++, it would be useful to be able to bind an object with its member function and produce a global function. Such features exist in other languages: for example, delegates in C# enable programmers to do exactly that. In C++, there are member function pointers. But they do not provide that feature. In this article I would like to propose a simple implementation of delegates, which uses C++ member function pointers and C++11 variadic templates. At present such implementation works only in GNU C++ 4.7.0. On Windows, you can use, for instance, MinGW to run GNU C++.

Background

The approach is to provide a function create_delegate, which can be called in one of the following ways:

  • create_delegate(&object, &member_function)
  • create_delegate(&function)

The first option creates an object with a member operator() that can be called as a function. The second produces a function pointer, which is the same as &function. Both these values are compatible with type type function<...>.

A Sample Program

Let's define a class with several methods

C++
class A
{
 int i;
public: 
 A(int k):i(k) {}

 auto get()const ->int { return i;} 
 auto set(int v)->void { i = v;}

 auto inc(int g)->int& { i+=g; return i;}
 auto incp(int& g)->int& { g+=i; return g;}

 auto f5 (int a1, int a2, int a3, int a4, int a5)const ->int
 {
  return i+a1+a2+a3+a4+a5;
 }

 auto set_sum4(int &k, int a1, int a2, int a3, int a4)->void
 {
  i+=a1+a2+a3+a4;
  k = i;
 }

 auto f8 (int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8) const ->int
 {
  return i+a1+a2+a3+a4+a5+a6+a7+a8;
 } 

 static auto sqr(double x)->double { return x*x; }

 auto g1(const int& n)->void { std::cout << "g1: " << n <<  std::endl; }
 auto g2(int&& n)->void { std::cout << "g2: " << n <<  std::endl; }
 auto g3(int& n)->int&  { n++; return n; }
 auto g4(int n)->int    { return 10*n; }
};

Please note that you do not have to use C++ syntax for functions using auto. You can define them using the "traditional" way. In the program, we can create a class as follows:

C++
A a(11);

Now, we can create delegates:

C++
auto set1 = create_delegate(&a,&A::set);
auto inc = create_delegate(&a,&A::inc);
std::function<int(int&)> incp = create_delegate(&a,&A::incp);
auto af5  = create_delegate(&a,&A::f5);
auto set_sum4= create_delegate(&a,&A::set_sum4);
auto af8  = create_delegate(&a,&A::f8);
auto sqr = create_delegate(&A::sqr); // static function, not a member
auto g1  = create_delegate(&a,&A::g1);  
auto g2  = create_delegate(&a,&A::g2);  
auto g3  = create_delegate(&a,&A::g3);  
auto g4  = create_delegate(&a,&A::g4);  

As demonstrated, we can use auto or function<...>. Now we can invoke the delegates:

C++
set1(25);
int x = 5;
int k = inc(x);
int k2 = incp(x);
k2 *= 7;
std::cout << "a.get():" << a.get() << std::endl;
std::cout << "k: " << k << std::endl;
std::cout << "x: " << x << std::endl;
std::cout << "af5(1,2,3,4,5): " << af5(1,2,3,4,5) << std::endl;

set_sum4(x,1,2,3,20);
std::cout << "after set_sum4(x,1,2,3,20)" << std::endl;
std::cout << "a.get(): " << a.get() << std::endl;
std::cout << "x: " << x << std::endl;
std::cout << "af8(1,2,3,4,5,6,7,8): " << af8(1,2,3,4,5,6,7,8) << std::endl;
std::cout << "sqr(2.1): " << sqr(2.1) << std::endl;
g1(100);
g2(32+5);
int p = 15;
int& g = g3(p);
int s = g4(35);
g++;
std::cout << "p: " << p << std::endl;
std::cout << "s: " << s << std::endl;

The program will print:

C++
a.get():30
k: 30
x: 35
af5(1,2,3,4,5): 45
after set_sum4(x,1,2,3,20)
a.get(): 56
x: 56
af8(1,2,3,4,5,6,7,8): 92
sqr(2.1): 4.41
g1: 100
g2: 37
p: 17
s: 350

Points about the implementation

For a simple member function that is not volatile or const, the implementation would be very simple. We have to create a class that will store the two pointers: one to the object and the other to the its member function:

C++
template <class T, class R, class ... P>
struct  _mem_delegate
{
    T* m_t;
    R  (T::*m_f)(P ...);
    _mem_delegate(T* t, R  (T::*f)(P ...) ):m_t(t),m_f(f) {}
    R operator()(P ... p) 
    {
            return (m_t->*m_f)(p ...);
    }
};   

But the return statement needs some correction. The problem is that if a function has an rvalue-reference argument (like auto g2(int&& n)->void), this will not work. Such a parameter in the body of the function is not an rvalue any more, it's an lvalue. There are two options here:

(1) to use forwarding return (m_t->*m_f)(std::forward<P>(p) ...);

(2) just to convert using static_cast return (m_t->*m_f)(static_cast<P>(p) ...);

Both options will work here. The first underlines the idea, the second is more general.

The variadic template allows us to define operator() with flexible number and types of parameters. The create_function implementation will simply return an object of this class:

C++
template <class T, class R, class ... P>
_mem_delegate<T,R,P ...> create_delegate(T* t, R (T::*f)(P ...))
{
    _mem_delegate<T,R,P ...> d(t,f);
    return d;
}

In practice, we need extra three implementations to cover cases of member functions which are const, volatile and const volatile. That's why traditional macros, using #define, are handy: they allow us to avoid rewriting the same code fragments. Here is the full implementation:

C++
template <class F>
F* create_delegate(F* f)
{
    return f;
}

#define _MEM_DELEGATES(_Q,_NAME)\
template <class T, class R, class ... P>\
struct _mem_delegate ## _NAME\
{\
    T* m_t;\
    R  (T::*m_f)(P ...) _Q;\
    _mem_delegate ## _NAME(T* t, R  (T::*f)(P ...) _Q):m_t(t),m_f(f) {}\
    R operator()(P ... p) _Q\
    {\
        return (m_t->*m_f)(std::forward<P>(p) ...);\
    }\
};\
\
template <class T, class R, class ... P>\
    _mem_delegate ## _NAME<T,R,P ...> create_delegate(T* t, R (T::*f)(P ...) _Q)\
{\
    _mem_delegate ##_NAME<T,R,P ...> d(t,f);\
    return d;\
}

_MEM_DELEGATES(,Z)
_MEM_DELEGATES(const,X)
_MEM_DELEGATES(volatile,Y)
_MEM_DELEGATES(const volatile,W) 

A more meaningful example: calculating the length of a curve

In this section I would like to consider a sample practical problem and look at various approaches, and show some alternative methods to delegates.

Calculation of the length of a curve: global functions

The function CurveLength can be used to calculate the length of a curve, which is defined by two functions fx(t) and fy(t), where t is in the range [a,b]. The parameter n is the number of step we take, and it affects the precision.

C++
auto CurveLength(auto (*fx)(double)->double, auto (*fy)(double)->double, 
   double a, double b,   int n = 20000) ->double
{    
    double s = 0.0;    
    double h = (b-a)/n;
    double t = a;
    double x0 = fx(t);
    double y0 = fy(t);        
    for (int i = 0; i < n; i++)
    {
        t += h;
        double x1 = fx(t);
        double y1 = fy(t);
        double dx = x1-x0;
        double dy = y1-y0;
        s += sqrt(dx*dx + dy*dy); 
        x0 = x1;
        y0 = y1;
    };    
    return s;
}

A simple C-style approach would be to define the curves we need as global functions, and if we need to parameterize the curve we will define global variables. We look at an ellipse and a cycloid. The cycloid length is easily calculated analytically. As for the ellipse, if the a and b axes are equal, it will be a circle, otherwise its length cannot be expressed by a simple formula. Below is our definition of the two curves:

C++
//Cycloid 

double cycloid_a;
auto Cycloid_fx(double t)->double
{
    return cycloid_a*(1 - cos(t));
}

auto Cycloid_fy(double t)->double
{
    return cycloid_a*(t - sin(t));
}

//Ellipse
double ellipse_a;
double ellipse_b;

auto Ellipse_fx(double t)->double
{
    return ellipse_a*cos(t);
}

auto Ellipse_fy(double t)->double
{
    return ellipse_b*sin(t);
} 

Here is a sample program to calculate their sizes:

C++
double PI = 4*atan(1.0);

ellipse_a = 1.0;
ellipse_b = 1.0;
    
cycloid_a = 1.0;
        
std::cout << std::setprecision(10) << std::setw(10);

std::cout <<  "ellipse1: " << 
  CurveLength(Ellipse_fx, Ellipse_fy, 0.0,2*PI) << std::endl;
std::cout <<  "cycloid1: " << 
  CurveLength(Cycloid_fx, Cycloid_fy, 0.0,2*PI) << std::endl;

ellipse_a = 3.0;
ellipse_b = 1.0;
    
cycloid_a = 5.0;
        
std::cout <<  "ellipse2: " << 
  CurveLength(Ellipse_fx, Ellipse_fy, 0.0,2*PI) << std::endl;
std::cout <<  "cycloid2: " << 
  CurveLength(Cycloid_fx, Cycloid_fy, 0.0,2*PI) << std::endl; 

The program will print:

ellipse1: 6.283185281
cycloid1: 7.999999992
ellipse2: 13.36489317
cycloid2: 39.99999996

The disadvantage of this approach is that all functions and their parameters are global.

An abstract class and virtual functions

A traditional, good C++ approach is to use an abstract class and define a member function CurveLength, which uses the member functions fx(t) and fy(t):

C++
class Curve
{
public:
    virtual auto fx(double t)->double = 0;
    virtual auto fy(double t)->double = 0;

    auto CurveLength(double a, double b, int n = 20000)->double
    {    
        double s = 0.0;    
        double h = (b-a)/n;
        double t = a;
        double x0 = fx(t);
        double y0 = fy(t);        
        for (int i = 0; i < n; i++)
        {
            t += h;
            double x1 = fx(t);
            double y1 = fy(t);
            double dx = x1-x0;
            double dy = y1-y0;
            s += sqrt(dx*dx + dy*dy); 
            x0 = x1;
            y0 = y1;
        };    
        return s;
    }
};

class Cycloid: public Curve
{
    double m_a;
public:
    Cycloid(double a):m_a(a) {}

    virtual auto fx(double t)->double
    {
        return m_a*(1 - cos(t));
    }

    virtual auto fy(double t)->double
    {
        return m_a*(t - sin(t));
    }
};

class Ellipse: public Curve
{
    double m_a;
    double m_b;
public:
    Ellipse(double a, double b):m_a(a),m_b(b) {}

    virtual auto fx(double t)->double
    {
        return m_a*cos(t);
    }

    virtual auto fy(double t)->double
    {
        return m_b*sin(t);
    }
}; 

The program will look like this:

C++
double PI = 4*atan(1.0);

Ellipse ellipse1(1.0,1.0);
Cycloid cycloid1(1.0);
        
std::cout << std::setprecision(10) << std::setw(10);

std::cout <<  "ellipse1: " << 
  ellipse1.CurveLength(0.0,2*PI) << std::endl;
std::cout <<  "cycloid1: " << 
  cycloid1.CurveLength(0.0,2*PI) << std::endl;

Ellipse ellipse2(3.0,1.0);
Cycloid cycloid2(5.0);        

std::cout <<  "ellipse2: " << 
  ellipse1.CurveLength(0.0,2*PI) << std::endl;
std::cout <<  "cycloid2: " << 
  cycloid1.CurveLength(0.0,2*PI) << std::endl; 

The problem is that all the curves have to be derived from the same base class, and if we want to use CurveLength in a library the class Curve should be part of it.

Using delegates

Delegates allow us to deal with unrelated classes. Here is the approach using delegates:

C++
auto CurveLength(std::function<double(double)> fx, 
  std::function<double(double)> fy, double a, double b, int n = 20000)->double
{    
    double s = 0.0;    
    double h = (b-a)/n;
    double t = a;
    double x0 = fx(t);
    double y0 = fy(t);        
    for (int i = 0; i < n; i++)
    {
        t += h;
        double x1 = fx(t);
        double y1 = fy(t);
        double dx = x1-x0;
        double dy = y1-y0;
        s += sqrt(dx*dx + dy*dy); 
        x0 = x1;
        y0 = y1;
    };    
    return s;
}

double PI = 4.0*atan(1.0);

class Cycloid
{
    double m_a;
public:
    Cycloid(double a):m_a(a) {}

    auto fx(double t)->double
    {
        return m_a*(1 - cos(t));
    }

    auto fy(double t)->double
    {
        return m_a*(t - sin(t));
    }
};

class Ellipse
{
    double m_a;
    double m_b;
public:
    Ellipse(double a, double b):m_a(a),m_b(b) {}

    auto getX(double t)->double
    {
        return m_a*cos(t);
    }

    auto getY(double t)->double
    {
        return m_b*sin(t);
    }
}; 

I have deliberately given different names to member functions. These two classes are unrelated. Here is a program:

C++
int main()
{
 
 Ellipse ellipse1(1.0,1.0);
 Cycloid cycloid1(1.0);
        
        auto ellipse1_fx = create_delegate(&ellipse1,&Ellipse::getX);
 auto ellipse1_fy = create_delegate(&ellipse1,&Ellipse::getY);   
        auto cycloid1_fx = create_delegate(&cycloid1,&Cycloid::fx);
 auto cycloid1_fy = create_delegate(&cycloid1,&Cycloid::fy);        

        std::cout << std::setprecision(10) << std::setw(10);

 std::cout <<  "ellipse1: " << 
   CurveLength(ellipse1_fx, ellipse1_fy, 0.0,2*PI) << std::endl;
 std::cout <<  "cycloid1: " << 
   CurveLength(cycloid1_fx, cycloid1_fy, 0.0,2*PI) << std::endl;

 Ellipse ellipse2(3.0,1.0);
 Cycloid cycloid2(5.0);
        auto ellipse2_fx = create_delegate(&ellipse2,&Ellipse::getX);
 auto ellipse2_fy = create_delegate(&ellipse2,&Ellipse::getY);   
        auto cycloid2_fx = create_delegate(&cycloid2,&Cycloid::fx);
 auto cycloid2_fy = create_delegate(&cycloid2,&Cycloid::fy);        

 std::cout <<  "ellipse2: " << 
   CurveLength(ellipse2_fx, ellipse2_fy, 0.0,2*PI) << std::endl;
 std::cout <<  "cycloid2: " << 
   CurveLength(cycloid2_fx, cycloid2_fy, 0.0,2*PI) << std::endl;


 return 0;
}

Delegates allow us to use the same functions for various curve classes, which are completely unrelated. This CurveLength can be part of the library.

An alternative approach: using lambdas

The C++11 lambdas make it possible to define functions immediately and also to capture the variables from the environment. The CurveLength and the curve classes can be defined exactly the same as in the example with delegates, but the program, the function calls change:

C++
int main()
{
	double PI = 4.0*atan(1.0);
	Ellipse ellipse1(1.0,1.0);
	Cycloid cycloid1(1.0);
        
        auto ellipse1_fx = [&ellipse1](double t){ return ellipse1.getX(t); };
	auto ellipse1_fy = [&ellipse1](double t){ return ellipse1.getY(t); };   
        auto cycloid1_fx = [&](double t){ return cycloid1.fx(t); };
	auto cycloid1_fy = [&](double t){ return cycloid1.fy(t); };        

        std::cout << std::setprecision(10) << std::setw(10);

	std::cout <<  "ellipse1: " << 
	  CurveLength(ellipse1_fx, ellipse1_fy, 0.0,2*PI) << std::endl;
	std::cout <<  "cycloid1: " << 
	  CurveLength(cycloid1_fx, cycloid1_fy, 0.0,2*PI) << std::endl;

	Ellipse ellipse2(3.0,1.0);
	Cycloid cycloid2(5.0);
        auto ellipse2_fx = [&ellipse2](double t){ return ellipse2.getX(t); };
	auto ellipse2_fy = [&ellipse2](double t){ return ellipse2.getY(t); };   
        auto cycloid2_fx = [&cycloid2](double t){ return cycloid2.fx(t); };
	auto cycloid2_fy = [&cycloid2](double t){ return cycloid2.fy(t); };       

	std::cout <<  "ellipse2: " << 
	  CurveLength(ellipse2_fx, ellipse2_fy, 0.0,2*PI) << std::endl;
	std::cout <<  "cycloid2: " << 
	  CurveLength(cycloid2_fx, cycloid2_fy, 0.0,2*PI) << std::endl;


	return 0;
}

I deliberately show two approaches to capture variables: a specific one [&ellipse1] , and general [&]. Either can be used. Lambdas are easy to use, but syntax becomes longer if you have to deal with more parameters. In the end, it's your choice.

Acknowlegment

I would like to thank all those of you who made comments on the previous versions of this article. I am especially grateful to Emilio Garavaglia for his valuable remark about perfect forwarding.

References

  1. http://www.codeproject.com/Articles/7150/Member-Function-Pointers-and-the-Fastest-Possible
  2. http://www.codeproject.com/Articles/11015/The-Impossibly-Fast-C-Delegates
  3. http://www.codeproject.com/Articles/13287/Fast-C-Delegate

License

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


Written By
Software Developer (Senior)
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Suggestion5* from me and a (small) suggestion Pin
adi.hodos17-Nov-13 22:54
adi.hodos17-Nov-13 22:54 
GeneralRe: 5* from me and a (small) suggestion Pin
Mikhail Semenov19-Nov-13 9:46
Mikhail Semenov19-Nov-13 9:46 
Yes, this looks good. It's in line with other features available in STL.
GeneralMy vote of 5 Pin
syu22-Jun-13 13:46
syu22-Jun-13 13:46 
QuestionLambda approach missing Pin
Stone Free24-May-13 5:50
Stone Free24-May-13 5:50 
AnswerRe: Lambda approach missing Pin
Mikhail Semenov24-May-13 7:22
Mikhail Semenov24-May-13 7:22 
GeneralMy vote of 5 Pin
dseres6-May-13 23:28
dseres6-May-13 23:28 
GeneralRe: My vote of 5 Pin
Mikhail Semenov8-May-13 22:12
Mikhail Semenov8-May-13 22:12 
QuestionWhy not std::function ? Pin
MttD21-Jan-13 8:02
MttD21-Jan-13 8:02 
AnswerRe: Why not std::function ? Pin
Mikhail Semenov11-Feb-13 8:51
Mikhail Semenov11-Feb-13 8:51 
QuestionRe: Why not std::function ? Pin
magicpapacy27-May-13 14:55
magicpapacy27-May-13 14:55 
AnswerRe: Why not std::function ? Pin
Mikhail Semenov27-May-13 22:27
Mikhail Semenov27-May-13 22:27 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA16-Jun-12 20:11
professionalȘtefan-Mihai MOGA16-Jun-12 20:11 
QuestionConsider perfect forwarding Pin
Emilio Garavaglia17-May-12 4:06
Emilio Garavaglia17-May-12 4:06 
AnswerRe: Consider perfect forwarding Pin
Mikhail Semenov18-May-12 10:27
Mikhail Semenov18-May-12 10:27 
GeneralMy vote of 5 Pin
Emilio Garavaglia17-May-12 4:02
Emilio Garavaglia17-May-12 4:02 
GeneralMy vote of 5 Pin
Pablo Aliskevicius16-May-12 18:56
Pablo Aliskevicius16-May-12 18:56 
GeneralMy vote of 5 Pin
revram15-May-12 18:26
revram15-May-12 18:26 
GeneralMy Vote of 5! Pin
Sharjith15-May-12 8:28
professionalSharjith15-May-12 8:28 
GeneralMy vote of 5 Pin
Aescleal15-May-12 6:50
Aescleal15-May-12 6:50 
GeneralRe: My vote of 5 Pin
Mikhail Semenov16-May-12 10:12
Mikhail Semenov16-May-12 10:12 
GeneralRe: My vote of 5 Pin
LostOfThought27-May-13 18:00
LostOfThought27-May-13 18:00 

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.