分享
 
 
 

Generic Callbacks

王朝other·作者佚名  2006-01-09
窄屏简体版  字體: |||超大  

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

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

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

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

剖析泛型回调

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的。作者可能并不想提供一个隐式的从T到callback<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年早期,在C,C++中就已经知道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_function或std::binary_function可以提供几个方便的、它所绑定的类型定义,它所依赖的相似的设施,但是这些仅在你准备使用函数对象的这些设施时它才有效。因为回调的自然属性以及它们会被如何使用,不太可能需要这些设施。(对于普通的单参,双参情况,如果将来它们应该可以这么使用,我们后来提到的单参,双参版本应该分别派生自std::unary_function和std::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. 可以被容器化。如果程序想保存一个回调对象为后来使用,那么它也可能会保存多个对象。如果它想把回调对象放入容器中,比如vector或list中会怎样?现在那是不可能的,因为回调对象不支持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_callback和std::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 );

}

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有