Generic Callbacks

         Generic Callbacks

             译自<<Exceptional C++ style>>的开放章节

JG问题

1.      设计、编写泛型设施时,预期什么样的质量?请解释。

Guru问题

2.      以下代码代表了一个有趣的、有用的惯用法来包装回调函数。请参看原文章来获得更详细的解释。[Kalev01]

指正这些代码并标出:

a)      可以改善风格的选择,以使设计满足更多的C++惯用法。

b)      限制了设施的可用性。

template < class T, void (T::*F)() >

class callback

{

public:

callback(T& t) : object(t) {} // assign actual object to T

void execute() {(object.*F)();} // launch callback function

private:

T& object;

};

解答

泛型质量

1.      设计、编写泛型设施时,预期什么样的质量?请解释。

泛型代码首先应该是可用的。那并不意味着它必须包含所有的操作。它的意思是泛型代码应改作合理的、平衡的努力以避免至少三件事:

1.    避免不适当的类型限制。例如,如果你想写一个泛型容器。很合理的要求是容器中的元素类型有一个拷贝构造函数,一个不会抛出异常的析构函数。但是缺省的构造函数呢?赋值运算符呢?用户想放入容器中的很多类型并不包含缺省的构造函数,并且如果你的容器使用它的话,那么这种类型将不能使用你编写的容器。所以你的容器并不是泛型的。(请参考Exceptional C++[Sutter00]来获得一个完全的例子。)

2.    避免不适合的功能限制。如果你编写一个设施来执行XY,但用户想执行Z,那会怎样?ZY的差距并不大。有时你会想让你的泛型设施更灵活,让它能够支持Z。有时你又不想那样。好的泛型设计的一部分是提供选择方式,意思是通过它,你的泛型设施可以被定制或扩展。这在泛型设计中是很重要的,并且你也不应该惊讶,因为这个原则在面向对象的类的设计中也是相同的。

基于策略的设计是众多重要技术中的一个,它允许在泛型代码中的“可插入”行为。基于策略的设计的例子请参看[Alexandrescu01]的部分章节。StartPtrSingleton是很好的开始点。这导致了一个相关的问题:

3.    避免不合适的[monolithic]设计。这个重要的问题并不会在下面要考虑的例子中出现,应该对它作进一个步的考虑,因此它不仅出现在本条款中,也出现在以后的条款中:请查看条款3740

在这三点中,你可能注意到重复出现的词是“不合适的”。它的意思是说:当在缺乏足够的泛型和过度工程之间做出平衡时,应该好好的判断。

剖析泛型回调

2.       以下代码代表了一个有趣的、有用的惯用法来包装回调函数。请参看原文章来获得更详细的解释。[Kalev01]

再看一下代码:

template < class T, void (T::*F)() >

class callback

{

public:

callback(T& t) : object(t) {} // assign actual object to T

void execute() {(object.*F)();} // launch callback function

private:

T& object;

};

现在,实际来讲,在这个有两个成员函数,每个成员函数都是一行的简单类中会有多少错误?结果是,这种极端的简单性就是问题的一部分。这个模板类不需要如此的重量级,根本不需要,但是可以用一个稍微轻量级的来代表它。

改善的风格

指正这些代码并标出:

a)      可以改善风格的选择,以使设计满足更多的C++惯用法。

你发现多少瑕疵?这是我总结的:

4.    构造函数应该是explict的。作者可能并不想提供一个隐式的从Tcallback<T>的转换。行为良好的类要避免给它的使用者产生潜在的错误。我们确切想要提供的如下:

explicit callback(T& t) : object(t){}//assign actual object to T

尽管我们已经分析了这个特定行,还有一个不是关于设计,而是关于描述的风格问题:

原则:尽量使构造函数是explicit,除非你确实想提供类型转换。

注释是错误的。注释中的“assign”是不正确的,所以导致某些误解。在构造函数中,更正确地是我们绑定T对象的一个引用,并扩展成回调对象。同样,经过了多次重读注释,我还不知道“to T”部分的意思。所以更好的注释将是“绑定实际的对象”

explicit callback(T& t) : object(t) {} // bind actual object

但是,所有注释所表达的正是代码已经表达的,确实非常可笑,它是无用注释的一个很好的例子,所以最好写成如下形式:

explicit callback(T& t) : object(t) {}

5.    execute函数因该是const的。毕竟,execute函数并没有对callback<T>对象的状态作任何改动。这有回到了基本的问题:Const正确性可能有点老,但是确实很好。至少自从1980年早期,在CC++中就已经知道Const正确性的价值。并且它的价值并不会因为我们到了新的千年而消失,也不会应为我们编写大量的模板而消失。

void execute()const{ (object.*F)(); }// launch callback function

原则:正确的使用const

尽管我们已经分析了execute函数,仍有一个更严重的惯用法问题:

6.    execute函数应该被写为operator()。在C++中,惯用法是用函数调用操作符来执行函数风格的操作。因此注释也变得无用了,可以安全的将其删除,因为我们现在的代码已经通过惯用法表达了它自己。

void operator()()const{(object.*F)();}//launch callback function

你可能会吃惊的说“如果我们提供了函数运算符,那它不就是一个函数对象了?”很好的问题,我们继续观察,作为一个函数对象,或许回调的实例应该是可配接的。

原则:提供operator()来代表惯用的函数对象,而不是提供一个名为execute的方法。

陷阱:(惯用法)难道这个回调不应该从std::unary_function继承吗?查看[Meyers01]的条款36来了解关于可配接的更多的讨论,以及为什么通常情况下它一个好的设计。但是,在这里,有两个很好的理由导致callback不应该从std::unary_function继承:

l       它不是单参函数。它不接收参数,单参函数接收一个参数。(void不算)

l       std::unary_function继承无论如何并不能使之可扩展。下面我们将要看回调或许应该能处理其他不同签名的函数;根据于参数的个数,或许根本没有合适的基类可以继承。例如,如果我们支持三个参数的callback,我们没有std::ternary_function来继承。

派生自std::unary_functionstd::binary_function可以提供几个方便的、它所绑定的类型定义,它所依赖的相似的设施,但是这些仅在你准备使用函数对象的这些设施时它才有效。因为回调的自然属性以及它们会被如何使用,不太可能需要这些设施。(对于普通的单参,双参情况,如果将来它们应该可以这么使用,我们后来提到的单参,双参版本应该分别派生自std::unary_functionstd::binary_function。)

修正机制错误及限制

b)    限制了设施的可用性。

7.    考虑使回调函数接收一个正常的参数,而不是一个模板参数。非类型模板参数很少使用,因为在编译时如此固定一个类型,能够获得的好处很少。因此我们替换为:

template < class T >

class callback {

public:

typedef void (T::*Func)();

callback(T& t, Func func) : object(t), f(func) {} // bind actual object

void operator()() const { (object.*f)(); } // launch callback function

private:

T& object;

Func f;

};

现在函数可以在运行时被改变,很容易添加一个成员函数以允许用户改变已存回调对象的函数,这在以前的版本中是不可能实现的。

原则:优先使用正常的函数参数,然后是非类型参数。除非它们确实是非类型模板参数。

8.     可以被容器化。如果程序想保存一个回调对象为后来使用,那么它也可能会保存多个对象。如果它想把回调对象放入容器中,比如vectorlist中会怎样?现在那是不可能的,因为回调对象不支持operator=,就是说它不能被赋值。为什么不能?因为它包含一个引用,在构造函数中,一旦绑定引用,它就不能再被修改了。

但是指针没有这样的限制,它可以被随意改变。在这种情况下,使用指向对象的指针而不是引用是安全的,并且可以使用编译器产生的拷贝构造函数和赋值运算符。

template < class T >

class callback {

public:

typedef void (T::*Func)();

callback(T& t, Func func) : object(&t), f(func) {} // bind actual object

void operator()() const { (object->*f)(); } // launch callback function

private:

T* object;

Func f;

};

现在,可能有形如list< callback< Widget, &Widget::Somefun > >这样的类型了。

原则:优先是你的对象和容器相兼容。特别的,为了将对象放入容器中,对象必须是可赋值的。

“但是,等一下”,你此时很想知道,“如果我有那样的一个list,为什么我不能有一个任意类型的回调呢?以便我可以记住它们并且当我想执行回调时,我可以随意执行”。实际上,如果你添加一个基类,你就可以那么做了。

9.    允许多态:为回调类型提供一个共通的基类。如果我们想让用户拥有list< callbackbase* >(或者更好list< shared_ptr<callbackbase> >),我们可以通过提供这样的一个基类来实现,基类中的运算符()什么也不做。

class callbackbase {

public:

virtual void operator()() const { };

virtual ~callbackbase() = 0;

};

callbackbase::~callbackbase() { }

template < class T >

class callback : public callbackbase {

public:

typedef void (T::*Func)();

callback(T& t, Func func) : object(&t), f(func) {} // bind actual object

void operator()() const { (object->*f)(); } // launch callback function

private:

T* object;

Func f;

};

现在,任何人可以保存list< callbackbase* > 并且可以多态的调用元素的运算符()。当然list< shared_ptr<callback> >将更好些;请参见[Sutter02b]

注意到,添加一个基类是一个权衡,但是却是很小的一个:当通过基类触发回调时,我们增加了两级间接调用的负荷,也就是说一个虚函数调用。但是这个负荷仅仅是通过使用基类时才有。不使用基类接口的代码并不需要为此付出。

原则:如果对你的模板类起作用的话,考虑允许多态以便你的类的不同的实例可以互交换的使用。如果的确起作用,那么为你所有的模板类提供一个共通的基类。

10.(惯用法,权衡)应该有一个辅助函数make_callback来进行类型推导。经过了一段时间使用之后,用户会对为临时对象提供显式模板参数感到厌倦。

list< callback< Widget > > l;

l.push_back( callback<Widget>( w, &Widget::SomeFunc ) );

为什么要写两次Widget?难道编译器不应该知道吗?可是,它不知道,但我们可以帮助它,让它知道此时仅仅需要临时变量。因此我们提供了一个辅助函数,它仅需要一个类型:

list< callback< Widget > > l;

l.push_back( make_callback( w, &Widget::SomeFunc ) );

make_callbackstd::make_pair的工作原理相同。make_callback函数应该是一个模板函数,因为这是编译器唯一可以推导的模板参数类型。如下是辅助函数:

template<typename T >

callback<T> make_callback( T& t, void (T::*f) () ) {

return callback<T>( t, f );

}

11.(权衡)添加其他函数签名的支持。我留下这个最大的工作。因为吟游诗人会说“会有更多的函数签名,而不仅仅是void ( T::*F)()!

原则:避免限制你的模板;避免为具体的类型或为更少的通用类型而硬编码。

如果限制的回调函数签名足够的话,无论如何要在这里结束了。如果我们不需要它,我们就不必将设计引向更复杂。如果我们确实需要它,我们将不得不将设计引向更复杂。

我不会写出所有的代码,因为那非常乏味。我所做的只是概要的描述你必须做的工作,以及如何做。

首先,常成员函数怎样?处理它的最容易的方式是提供一个同等的使用const签名的回调,在那个版本中,注意要保存一个执行const对象的指针或引用。

其次,如果函数的返回类型不为void会怎样?最简单的方式是添加另外一个模板参数表示返回类型。

最后,如果回调函数需要接收参数会怎样?再次,添加模板参数,注意也给操作符()添加函数参数。

记着添加新的模版来处理每种潜在数量的回调参数。

探索完毕所有的代码,你不得不限制回调支持的函数参数的个数。或许在将来的C++0x中,我们提供一个想varargs的模版来帮助你处理这些事情,但是现在没有。

总结

把所有的东西放在一起,做些纯粹的风格调整,使用typename,命名规则,以及我比较喜欢的空格规则,这就是我们得到的最终结果:

class CallbackBase {

public:

virtual void operator()() const { };

virtual ~CallbackBase() = 0;

};

CallbackBase::~CallbackBase() { }

template<typename T>

class Callback : public CallbackBase {

public:

typedef void (T::*F)();

Callback( T& t, F f ) : t_(&t), f_(f) { }

void operator()() const { (t_->*f_)(); }

private:

T* t_;

F f_;

};

template<typename T>

Callback<T> make_callback( T& t, void (T::*f) () ) {

return Callback<T>( t, f );

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值