一个问题,两人讨论,几行代码,一些启发
By
刘未鹏
(pongba)
Shen
:请教一个问题,我简化一下,发给你:
Pongba
:
OK
Shen:
就这样发给你吧,不多,我描述一下:
template<class T> class A { };
class B { public: A * m_task; };
class C: public B
{
public:
C();
C(A*);
};
typedef A<C> D;
C::C(A* p):m_task(p){};
定义了这几个类。
C::C(A * p) : m_task (p)
这句,是编译不过的,因为
A
是定义的模板类,该如何写?
Pongba:
当然不通过啊。你没给模板参数啊,
A
是模板类,要模板参数啊。
Shen:
是的,但是我写
C::C(D * p): m_task ( p )
也是不对,似乎总是循环的,这样的定义的有问题,因为
A<C>
这时候
C
的构造函数中做,但是
C
还没有构造出来。
Pongba:
你要考虑的是你想表达的概念,类只是一个手段;首先明确这三个类的语意,而不是语法。
Shen: typedef A<C> D;
这里,我都多个不同的业务,所以定义不同的
C
。
Pongba: A
的语意是什么?
Shen: A
是个基类,
D
是
A
的不同业务,比如
D1,D2
,
...
Pongba: -_-||A
当然是基类,我想知道的是
A
的目的是什么,
A
为什么有一个模板参数。这个模板参数放在那儿的初衷是什么。
Shen: class B { public: A * m_task; }
所以
B
中,有这样的定义。是希望通过
C::C(A * p) : m_task(p)
,将
B
中的
m_task
指向
A
(当然是
A
的不同业务)。
A
的模板参数的原因,是因为我希望后面
typedef A<C> D;
Pongba: B
的成员不应该由
C
初始化,破坏了封装原则。
Shen:
也许还有
C1 ,C2 ,
所以希望
typedef A<C1> D1; typedef A<C2> D2;
因为有多个
C,
(如
C1,C2,C3...
)
Pongba:
问题是为什么你希望
typedef A<C> D;
我想知道的是
A
是个什么类。他的功能是什么。这么说吧,
STL
的
vector
,他的语意是
“
一组元素
”
,他为什么要一个模板参数,是因为要
“
参数化
”
这组元素的类型。那么
A
的语意是什么,他为什么要一个模板参数。
Shen: A
的作用是,有
virtual
函数,调用不同的业务。
Pongba: A
是
“
业务
”
类吗?
Shen:
错了,
B
是业务基类,刚才说错了。
C
是从
B
继承的,因为有多个
C
业务,比如
C1,C2...
Pongba:
好,
B
是业务基类,所有从
B
继承的都是具体的业务类,
Ci
。这个理解对吧?
Shen: right
Pongba:
那么,还是那个问题,
A
的作用呢?是
“
调用业务基类
”
吗?也就是说
A
的
“
职责
”
是什么。
先把语言放在一边,考虑你的设计。
Shen: A
的作用主要是线程处理。我再解释一下:
A
的作用,是把
B
的业务放入队列,取出处理等操作。至于怎么处理,就是
B
处理了,
A
只负责入出队列等操作。
A
有自己的队列和定时器等。
Pongba:
也就是说,
A
是一个任务调度器。只负责调度逻辑。对否?
Shen:
对。
Pongba:
好,那下一个问题。
B
里面为何要指向
A
。
Pongba:
哦,等等,先回答这个问题:
A
里面的那个
“
队列
”
是如何实现的。
Shen:
唉,这个就是前人设计的不好了:
Pongba:
是异构队列吗?
Shen:
队列倒是很独立。
Pongba:
异构的意思就是,能存放不同的派生自
B
的任务吗?比如
C1,C2…
Shen:
与这个地方没有关系。
Pongba:
这么说吧,
A
的模板参数
T
,在哪个地方被用到了?
Shen:
我还没有说完:前人设计的不好的原因是:
A
队列中放的是
B
。
B
中有:
m_task->enqueue( this )
。你可以把
m_task
看做一个队列处理的东东。
Pongba:
晕,也就是说一个
task
创建起来是自动
enque
的,不早说
:)
Shen:
是的,所以这个是以前代码的风格不好之处。
Pongba: m_task->enqueue( this )
是发生在
B
的构造函数中的没错吧
Shen:
非也,在一个普通的函数中。普通的成员函数。
Pongba:
嗯,理解。必要的时候才放入调度队列。是吧。让用户有手动调用任务对象的某个函数将它放入队列的自由。
Shen:
是的。
Pongba:
那还是回答刚才那个问题:
A
的模板参数
T
,在什么地方被用到了?
Shen: C::C(A * p) : m_task(p)
就是这里,用错了,不知道该如何用。
Pongba:
唉,我猜你误会原有代码的结构了。你翻开
A
类的代码。
Shen: B
中,应该知道
task
是哪种
TASK
Pongba:
找到
A
里面什么地方用到了模板参数
T
。如果不出意外因该是类似于
std::vector<T*> v;
。
Shen:
原来没有,是最近增加了业务。所以准备用模板,简化一下,不要写多个业务处理类。
Pongba:
也就是说这个模板参数是你自己加上去的
?
Shen: right
。
Pongba:
那首先你要明白用模板的意义。你准备用模板之前的动机是什么?也就是说,需求是什么?或者说,你面临了什么问题,才想要用模板的。
Shen:
主要这段,有循环嵌套使用的地方,所以不好处理。
Pongba:
循环嵌套,是有深层原因的。不是语法问题。是设计问题。所以我现在正试图用问问题的方法来帮你找出根本原因。
Shen:
是的,设计的时候没有考虑细节。
Pongba:
不是,是设计的时候没有在抽象层考虑好就一下潜入到语言层面。这样的问题在论坛上很常见,尤其是遇到模板的时候。我现在需要你提供更多的上下文。如果我猜得不错,
A
里面应该有一个
B*
的容器。这是一个异构容器,里面可以存放所有派生自
B
的类的对象。
Shen:
唉,实际开发中,一般设计和开发都是分开的
:)
我设计别人写,别人设计我写。
Pongba:
用一段简单的代码表达你当时面临的问题。
Shen:
我来写一下,你等一会,大概要一段时间,我把业务逻辑去掉。
Shen:
我去掉业务后,代码如下:
template<class T> class A{ int enqueue(B * p) { //
放入队列
}
//
另一个线程,取出队列中的数据
p,
然后
p->process(); }
class B { public: A * m_task; m_task->enqueue( this );
virtual process(); }
class C: public B { public: C(); virutal process() { //
自己的业务
} }
Pongba:
那现在有什么问题呢,为什么要吧
A
做成模板呢
?
Shen:
因为
C
有多个,所以
typedef A<C> D; typedef A<C
1
> D
1
; typedef A<C2> D2;
因为不同的
C,
希望不同的
A,
所以
typedef A<C> D;
Pongba: C
有多个有什么问题吗?只要都继承自
B
,就都可以放到
A
里面。你程序里面是不是有一个全局的
A
对象?
Shen:
错了。
Pongba:
问题是,不同的
C
为什么希望要不同的
A
?
Pongba:
不同的
C
是可以放到同一个
A
对象中的。因为他们都继承自
B
,而
A
对象里面是一个保存
B*
的容器。现在,你回忆一下,最初的问题是什么,也就是说,干嘛想起来要去把
A
模板化
。难道就是因为
“
不同的
C”
?
A
本来就可以处理
“
不同的
C”
啊,别忘了。
Shen:
似乎有点眉目。
Pongba: A
内部本就是一个异构容器,所有不同的
C
只要继承自
B
,都可以放在
A
里面。
Shen:
我来看看设计文档,当时为什么定义成多个
A
。
Shen:
实际应用中
A
是处理线程的,
D1
,
D2
,
D3
分别是处理
CDMA
,
GPRS
,
ADSL
的。我来看看当时设计的时候,怎么考虑的。所以写了这句:
typedef A<C> D;
Pongba:
是不是说,
CDMA
,
GPRS
,
ADSL
分别是三类不同的任务?
Shen: D1
,
D2
,
D3
都是
typedef A<C> D;
为了起线程和队列的。但是线程和队列与业务无关,为什么要
typedef A<C> D;
?
Pongba: typedef A<C> D;
是谁写的?是不是说,
CDMA ,GPRS, ADSL
分别是三类不同的任务?
Shen:
设计的时候写的,所以我在理解
:)
Pongba: “C”
在文档中的实际名字是什么?文档中写的就是
“typedef A<C> D”
吗?
Shen:
当然不是
:)
呵呵,我是为了简化,所以这么写的。
CADSLBillMsg
。
Pongba:
那还不给我实际的名字,名字有含义的呀老大
。
Shen:
这个是
C
的一个名字
:)
Pongba:
唉
…
忙活半天,这不是简化,是丢失信息啊。
Shen:
写名字,太长,不容易理解
Pongba: …
Pongba: CADSLBillMsg
是不是仍然是用作基类的。你说有
C1
,
C2
,
C3
,是不是个代表
CDMA
,
GPRS
,
ADSL
这三种任务?比如
CADSLBillMsg
是所有
ADSL
任务的基类?对不对?
Shen:
发给你吧,会看的清楚一些
:)
Pongba:
好吧。
Shen:
template<class T> class CBillTask
{
int enqueue(B * p)
{
//
放入队列
}
//
另一个线程,取出队列中的数据
p
,然后
p->process();
}
class CBillMsg
{
public:
CBillTask
* m_task;
m_task->enqueue( this );
virtual process();
}
class CADSLBillMsg: public CBillMsg
{
public:
CADSLBillMsg();
virutal process()
{
//
自己的业务
}
}
typedef CBillTask<CADSLBillMsg> CADSLBilTask;
CADSLBillMsg::CADSLBillMsg(CBillTask * p):m_task(p)
Pongba:
老问题:原来
CBillTask
不是模板类。现在做成模板类。动机是什么
?
Shen:
因为
CBillTask
原来是处理
CDMA
业务的,现在有
ADSL
和
GPRS
业务,所以想做成模板类。
Pongba:
哦,早说这个不就好了。
-_-||
Shen:
呵呵。
Pongba:
也就是说,原来只有
CCDMABillMsg
。现在多了
CADSLBillMsg
和
CGPRSBillMsg
。是不是?
Shen:
原来只有
CBillMsg
。现在将
CDMA
业务也拿出来,将
CBillMsg
作为基类,然后
CCDMABillMsg
、
CADSLBillMsg
和
CGPRSBillMsg
作为业务处理类。
Pongba:
大致知道了。现在有一个问题。你想不想同一个
CBillTask
对象内部既存放
CCDMABillMsg
的对象又存放
CADSLBillMsg
对象?
这样,我写一段代码,你看猜测得对不对。
Shen:
好的。
Shen:
template <class BILMSG>
int CBillTask<BILMSG>::handle_timeout(
const ACE_Time_Value & tv, const void * arg)
{
BILMSG *pMsg = new BILMSG(this);
// …
return 0;
}
CBillTask
中希望
new
不同的业务对象,所以模板可以简化。因为不想写多个
CBillTask
,所以用了
typedef
。希望通过模板,将业务类型
(CADSLBillMsg ...)
传递进去。
Pongba:
你是不是希望一个特定的
CBillTask
对象中只能存放同一类
BillMsg
,比如只能存放
CADSLBillMsg
。
Shen:
是的。一个特定的
CBillTask
对象,只存放一类
BillMsg
。
Pongba:
那就好办。等等。
template<typename T>
class CBillTask
{
public:
int enqueue(T* p);
private:
list<T*> cont_;
};
class CADSLBillMsg
{
public:
CADSLBillMsg();
CADSLBillMsg(CBillTask<CADSLBillMsg>* task) : m_task(task) { }
virutal process() { // ... }
private:
CBillTask<CADSLBillMsg>* m_task;
};
typedef CBillTask<CADSLBillMsg> CADSLBillTask;
这样的话,
CBillMsg
基类就不要了。
Shen:
我来看看
:)
基类中,有大量的
virtual
函数,在业务类中继承,如果把基类去掉,在这种应用场景下,可能不适合。
Pongba:
嗯,基类是抽象类吗?
Shen:
目前修改的目的是做成抽象类。以前是被实例化的,现在就是要抽象出来。不会被实例化。
Pongba:
那现在有两个办法:
1.
把基类中的成员
m_task
移到具体的业务派生类中。
2.
保留基类原来的样子,牺牲(静态)类型安全性。
Shen:
第一个方法不错!怎么说?
Pongba:
第一个方法,派生类将变成这个样子:
class CADSLBillMsg : CBillMsg
{
public:
CADSLBillMsg();
CADSLBillMsg(CBillTask<CADSLBillMsg>* task) : m_task(task) { }
virutal process() { // ... }
private:
CBillTask<CADSLBillMsg>* m_task;
};
Shen:
这个不错,解决了我的问题。
Pongba:
注意
m_task
的类型。
CBillTask<CADSLBillMsg>*
。也就是说,
CADSLBillMsg
对象只能被放在
CBillTask<CADSLBillMsg>
对象中。类型安全性。
Shen:
是的,我的需求是这样的。
Pongba:
如果一个
CCDMABillMsg
对象试图把它自己往一个
CBillTask<CADSLBillMsg>
对象中放的话,就会出错。
Shen:
呵呵,如我所想。
Pongba:
第二个办法就是保持基类,牺牲类型安全性。我也简单说一下,你对比一下。以免我对你的需求有误解。
Shen:
好。
Pongba:
第二个办法更简单了其实。
class CBillMsg;
class CBillTask
{
public:
int enqueue(CBillMsg* p);
private:
list<CBillMsg*> cont_;
};
class CBillMsg {
public:
CBillMsg(CBillTask* task) : m_task(task) { }
private:
CBillTask* m_task;
};
class CADSLBillMsg : CBillMsg
{
public:
CADSLBillMsg();
CADSLBillMsg(CBillTask* task) : CBillMsg(task) { }
virutal process() { // ... }
};
CBillTask adslBillTask;
不用参数化
CBillTask
,依赖于用户忠实地往
adslBillTask
对象中只放
CADSLBillMsg
。如果用户不小心放了
CCDMABillMsg
,编译不会报错。(当然,也可以设置运行期检查)。这个是不用模板的办法。
Shen:
但是,我得这个需求:
template <class BILMSG>
int CBillTask<BILMSG>::handle_timeout(
const ACE_Time_Value & tv, const void * arg)
{
BILMSG *pMsg = new BILMSG(this);
// …
return 0;
}
就无法解决了。
Pongba:
哦,对的。忘了这个了。
Shen:
在
CBillTask
中,我要
new
一个具体类型的
BillMsg
。
Pongba:
也就是说,
CBillTask
必须知道放在它内部的具体类型。怎么会有这么怪异的需求,架构有问题。
Shen:
是的,前人的架构设计的不好。导致后面重构起来非常麻烦。
Pongba: CBillTask
应该不耦合于任何特定的
Task
类型才对。刚才展示的那行代码显示
CBillTask
耦合于具体
BillMsg
类啊。
Shen:
是的,所以才搞了一个模板类。
Pongba: handle_timeout
的作用是什么?
Shen:
一个定时器,定时处理具体的业务。然后掉
BillMSg
的函数,
BillMsg
发现是调用入队列,再入
BillTask
的队列
Pongba:
最初的需求是不是就是由新添的这个
handle_timeout
函数驱动的?
另外,那他要
new
具体的
MSG
类干嘛捏?调用
p->process
不就行了?
Shen:
因为这个定时器,要定时的将业务
new
出来,然后处理之。所以需要知道
new
出来的是哪个业务。
Pongba:
业务不是已经存在
BillTask
的内部了吗?要不然那个
enqueue
干嘛的?
Shen: Billtask new
出来后,需要给
BillMsg
。然后
BillMsg
发现是一个需要入队列的消息,再入队列。然后
Billtask
再起线程取出队列中的消息。所以
BillTask
希望知道自己
new
出来的是哪个业务对象。
Pongba:
这个
…
Shen:
无语吧。
Pongba:
我问你,是不是具体的业务类都是由
BillTask
创建的?
Shen:
是的。
Pongba:
都是由
handle_timeout
创建出来?
Shen:
可以这么说。
handle_timeout
负责定时创建各种业务
Pongba:
那每次
timeout
的时候都往队列里面塞仅仅一个业务类对象,然后立即取出来处理?如果队列里面仅有一个对象,那要队列干嘛?
Shen:
不是那么简单,当然我简化了,放入队列后,再取出来,让另外一个线程处理。因为处理业务时间很长。防止被阻塞住。
Pongba:
这个架构,以后有得你烦呢。
Shen:
是的,已经让我痛苦了很久了。
Pongba:
把
CBillTask
做成一个业务无关的
scheduler
类,这个类只负责定时处理,然后时间到就调用一个
command
。把
new Msg
然后
process
的这些逻辑封装成一个
command
类,给那个
scheduler
类处理。
Shen:
但是数万行代码
…
总结
朋友的问题解决了吗?我觉得远远没有。就像不好的代码一样,不好的结构能够感觉出来,别扭。把
m_task
移到派生类里面只是一贴膏药,并没有根除病灶。
CCDMABillMsg
往下是不是会再派生类了?如果派生了的话就会在同一个对象中出现两个
m_task
,一个是派生类的,另一个是基类
CCDMABillMsg
子对象的。
CBillTask
究竟是不是应该做成模板呢?
m_task
是不是仍然应该(以
void*
形式?)存放在基类中,并由特定的派生类
D
强转为相应的
CBillTask<D>*
呢?(毕竟
m_task
从语意上是属于“所有
BillMsg
类的信息,只不过不同的
BillMsg
类眼里看到的
m_task
的类型不一样而已)。再比如
m_task
这个成员存在的意义是什么,是不是一定要它呢?
CBillTask::handle_timeout
是不是应该用
delegate
来实现,从而使其不依赖于具体的
BillMsg
类,并且从而
CBillTask
也就不必要成为模板类了呢?
但是
…
但是有数万行代码呢,老大
…
一些
感想
2.
静态和强类型系统是一把双刃剑,其伤人的那一面是容易导致为了敷衍类型系统而作出的错误设计。
3.
抽象层面的错误往往最终以相差了十万八千里的低层错误表现出来(如类型错误),导致纠错困难。一个抽象层次高的语言对此可以带来帮助。从该意义上,
C++ Concepts
是一个本质进步。