C++11 中委托的实现。






4.93/5 (20投票s)
这是“成员函数指针和最快的 C++ 委托”的替代方案。
引言
在 C++ 中,能够将一个对象与其成员函数绑定并生成一个全局函数会很有用。其他语言中存在此类功能:例如,C# 中的委托允许程序员完成此操作。在 C++ 中,存在成员函数指针,但它们不提供此功能。在本文中,我想提出一种委托的简单实现,它使用 C++ 成员函数指针和 C++11 可变参数模板。目前,此实现仅在 GNU C++ 4.7.0 中有效。在 Windows 上,您可以使用 MinGW 来运行 GNU C++。
背景
该方法是提供一个名为 create_delegate
的函数,可以按以下方式调用:
create_delegate(&object, &member_function)
create_delegate(&function)
第一种选择创建一个具有 operator()
成员的对象,该对象可以像函数一样被调用。第二种选择生成一个函数指针,这与 &function
相同。这两种值都与 function<...>
类型兼容。
一个示例程序
让我们定义一个具有多个方法的类
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; }
};
请注意,您不必为使用 auto 的函数使用 C++ 语法。您可以使用“传统”方式定义它们。在程序中,我们可以按如下方式创建类:
A a(11);
现在,我们可以创建委托了
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);
如所示,我们可以使用 auto 或 function<...>。现在我们可以调用委托了
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;
程序将打印
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
关于实现的几点说明
对于一个简单的非 volatile 或 const 的成员函数,实现会非常简单。我们必须创建一个类来存储两个指针:一个指向对象,另一个指向其成员函数
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 ...);
}
};
但是 return 语句需要一些修正。问题是,如果一个函数具有右值引用参数(如 auto g2(int&& n)->void),这将不起作用。函数体中的此类参数不再是右值,而是左值。这里有两种选择:
(1) 使用转发 return (m_t->*m_f)(std::forward<P>(p) ...);
(2) 仅使用 static_cast 进行转换 return (m_t->*m_f)(static_cast<P>(p) ...);
这两种选择在此处都适用。第一种强调了理念,第二种更通用。
可变参数模板允许我们定义具有灵活数量和类型的参数的 operator()
。 create_function
的实现将简单地返回该类的对象
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;
}
实际上,我们需要另外三个实现来覆盖 const、volatile 和 const volatile 成员函数的场景。这就是为什么传统的宏,使用 #define
,很有用:它们允许我们避免重写相同的代码片段。这是完整的实现:
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)
一个更有意义的例子:计算曲线的长度
在本节中,我想探讨一个实际的示例问题,并研究各种方法,以及展示委托的一些替代方法。
曲线长度计算:全局函数
函数 CurveLength
可用于计算曲线的长度,该曲线由两个函数 fx(t) 和 fy(t) 定义,其中 t 在 [a,b] 范围内。参数 n 是我们采取的步数,它影响精度。
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;
}
一种简单的 C 风格方法是定义所需的曲线作为全局函数,如果我们想参数化曲线,我们将定义全局变量。我们来看一个椭圆和一个摆线。摆线的长度可以很容易地通过解析法计算。至于椭圆,如果 a 和 b 轴相等,它将是一个圆,否则其长度无法用简单的公式表示。以下是我们对两条曲线的定义:
//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);
}
这是计算其大小的示例程序:
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;
程序将打印
ellipse1: 6.283185281 cycloid1: 7.999999992 ellipse2: 13.36489317 cycloid2: 39.99999996
这种方法的缺点是所有函数及其参数都是全局的。
抽象类和虚函数
一种传统的、好的 C++ 方法是使用抽象类并定义一个成员函数 CurveLength
,该函数使用成员函数 fx(t) 和 fy(t)
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);
}
};
程序将如下所示:
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;
问题在于,所有曲线都必须继承自同一个基类,如果我们想在库中使用 CurveLength
,那么 Curve 类将成为其一部分。
使用委托
委托使我们能够处理不相关的类。这是使用委托的方法:
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);
}
};
我故意为成员函数赋予了不同的名称。这两个类是不相关的。这是一个程序:
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;
}
委托允许我们为各种完全不相关的曲线类使用相同的函数。这个 CurveLength
可以成为库的一部分。
一种替代方法:使用 lambda 表达式
C++11 的 lambda 表达式使得可以立即定义函数,并且还可以捕获环境中的变量。CurveLength
和曲线类可以与委托示例中的完全相同地定义,但程序和函数调用会有所不同:
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;
}
我故意展示了两种捕获变量的方法:一种特定的 [&ellipse1],一种通用的 [&]。两者都可以使用。Lambda 表达式易于使用,但如果您需要处理更多参数,语法会变得更长。最终,这取决于您的选择。
致谢
我想感谢所有对本文早期版本发表评论的人。我尤其感谢 Emilio Garavaglia 对我的完美转发的宝贵意见。