- 浏览: 4185610 次
最新评论
一个问题,两人讨论,几行代码,一些启发
一个问题,两人讨论,几行代码,一些启发
By 刘未鹏(pongba)
刘言|C++的罗浮宫(http://blog.csdn.net/pongba)
Shen:请教一个问题,我简化一下,发给你:
Pongba:OK
Shen: 就这样发给你吧,不多,我描述一下:
template<class t> class A { };<p></p></class>
class B { public: A * m_task; };
class C: public B
{
public:
C();
C(A*);
};
typedef A<c> D; <p></p></c>
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的构造函数中做,但是C还没有构造出来。
Pongba: 你要考虑的是你想表达的概念,类只是一个手段;首先明确这三个类的语意,而不是语法。
Shen: typedef A<c> D;</c> 这里,我都多个不同的业务,所以定义不同的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;<p></p></c>
Pongba: B的成员不应该由C初始化,破坏了封装原则。
Shen: 也许还有C1 ,C2 ,所以希望 typedef A<c1> D1; typedef A<c2> D2; </c2></c1>因为有多个C,(如C1,C2,C3...)
Pongba: 问题是为什么你希望typedef A<c> D;</c> 我想知道的是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;</t>。
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) { //</class>放入队列 } //另一个线程,取出队列中的数据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><span style="FONT-SIZE: 12pt; FONT-FAMILY: 宋体; mso-ascii-font-family: 'Century Gothic'">1</span><span lang="EN-US" style="FONT-SIZE: 12pt; FONT-FAMILY: " century gothic>> D</span><span style="FONT-SIZE: 12pt; FONT-FAMILY: 宋体; mso-ascii-font-family: 'Century Gothic'">1</span><span lang="EN-US" style="FONT-SIZE: 12pt; FONT-FAMILY: " century gothic>; typedef A<c2> D2; </c2></span><span style="FONT-SIZE: 12pt; FONT-FAMILY: 宋体; mso-ascii-font-family: 'Century Gothic'">因为不同的</span><span lang="EN-US" style="FONT-SIZE: 12pt; FONT-FAMILY: " century gothic>C,</span><span style="FONT-SIZE: 12pt; FONT-FAMILY: 宋体; mso-ascii-font-family: 'Century Gothic'">希望不同的</span><span lang="EN-US" style="FONT-SIZE: 12pt; FONT-FAMILY: " century gothic>A,</span><span style="FONT-SIZE: 12pt; FONT-FAMILY: 宋体; mso-ascii-font-family: 'Century Gothic'">所以</span><span lang="EN-US" style="FONT-SIZE: 12pt; FONT-FAMILY: " century gothic>typedef A<c> D;<p></p></c></span></c></c>
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;<p></p></c>
Pongba: 是不是说,CDMA,GPRS,ADSL分别是三类不同的任务?
Shen: D1,D2,D3都是typedef A<c> D; </c>为了起线程和队列的。但是线程和队列与业务无关,为什么要typedef A<c> D;</c>?
Pongba: typedef A<c> D;</c>是谁写的?是不是说,CDMA ,GPRS, ADSL分别是三类不同的任务?
Shen: 设计的时候写的,所以我在理解:)
Pongba: “C”在文档中的实际名字是什么?文档中写的就是“typedef A<c> D”</c>吗?
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<p></p></class>
{
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;<p></p></cadslbillmsg>
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><p></p></class>
int CBillTask<bilmsg>::<strong style="mso-bidi-font-weight: normal">handle_timeout</strong>(<p></p></bilmsg>
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><p></p></typename>
class CBillTask
{
public:
int enqueue(T* p);
private:
list<t> cont_; <p></p></t>
};
class CADSLBillMsg
{
public:
CADSLBillMsg();
CADSLBillMsg(CBillTask<cadslbillmsg>* task) : m_task(task) { } <p></p></cadslbillmsg>
virutal process() { // ... }
private:
CBillTask<cadslbillmsg>* m_task; <p></p></cadslbillmsg>
};
typedef CBillTask<cadslbillmsg> CADSLBillTask;<p></p></cadslbillmsg>
这样的话,CBillMsg基类就不要了。
Shen: 我来看看:) 基类中,有大量的virtual函数,在业务类中继承,如果把基类去掉,在这种应用场景下,可能不适合。
Pongba: 嗯,基类是抽象类吗?
Shen: 目前修改的目的是做成抽象类。以前是被实例化的,现在就是要抽象出来。不会被实例化。
Pongba: 那现在有两个办法:
1. 把基类中的成员m_task移到具体的业务派生类中。
2. 保留基类原来的样子,牺牲(静态)类型安全性。
Shen: 第一个方法不错!怎么说?
Pongba: 第一个方法,派生类将变成这个样子:
class CADSLBillMsg : CBillMsg
{
public:
CADSLBillMsg();
CADSLBillMsg(CBillTask<cadslbillmsg>* task) : m_task(task) { } <p></p></cadslbillmsg>
virutal process() { // ... }
private:
CBillTask<cadslbillmsg>* m_task; <p></p></cadslbillmsg>
};
Shen: 这个不错,解决了我的问题。
Pongba: 注意m_task的类型。CBillTask<cadslbillmsg>*</cadslbillmsg>。也就是说,CADSLBillMsg对象只能被放在CBillTask<cadslbillmsg></cadslbillmsg>对象中。类型安全性。
Shen: 是的,我的需求是这样的。
Pongba: 如果一个CCDMABillMsg对象试图把它自己往一个CBillTask<cadslbillmsg></cadslbillmsg>对象中放的话,就会出错。
Shen: 呵呵,如我所想。
Pongba: 第二个办法就是保持基类,牺牲类型安全性。我也简单说一下,你对比一下。以免我对你的需求有误解。
Shen: 好。
Pongba: 第二个办法更简单了其实。
class CBillMsg;
class CBillTask
{
public:
int enqueue(CBillMsg* p);
private:
list<cbillmsg> cont_; <p></p></cbillmsg>
};
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><p></p></class>
int CBillTask<bilmsg>::handle_timeout(<p></p></bilmsg>
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>*</d>呢?(毕竟m_task从语意上是属于“所有BillMsg类的信息,只不过不同的BillMsg类眼里看到的m_task的类型不一样而已)。再比如m_task这个成员存在的意义是什么,是不是一定要它呢?CBillTask::handle_timeout是不是应该用delegate来实现,从而使其不依赖于具体的BillMsg类,并且从而CBillTask也就不必要成为模板类了呢?
但是…但是有数万行代码呢,老大…
一些感想
(一些想法,昨天上不了网就短信发到了fanfou上,抄录在这里)
1. 语法层面的(nontrivial的)错误往往预示着语意层面的错误。例如,循环依赖导致的语法错误往往暗示抽象设计存在问题。(又:参见云风老大的意见)
2. 静态和强类型系统是一把双刃剑,其伤人的那一面是容易导致为了敷衍类型系统而作出的错误设计。
3. 抽象层面的错误往往最终以相差了十万八千里的低层错误表现出来(如类型错误),导致纠错困难。一个抽象层次高的语言对此可以带来帮助。从该意义上,C++ Concepts是一个本质进步。
相关推荐
MotionKit - 只用两行或几行代码就能够从加速度计,陀螺仪和磁力计获取数据
对网页进行简繁字体转换的方法一般有两种:一是使用《简繁通》这样的专业软件,另外一种是制作两套版本的网页。...在这里给大家提供一个非常简单的方法,只须在页面上添加几行代码就可以轻松搞定网页的简繁转换了。
#[MotionKit](http://goo.gl/bpXBlO)—缺少的iOS包装器:musical_notes:现在,您可以以神奇的方式从加速度计,磁力计,陀螺仪和设备运动中获取数据,只需两行或几行代码。 完全兼容#[MotionKit]...
用数组输出星星的问题,当读入行数时,则输入几行后就可输出第一行有一个星,第二行有两个,第n行有n个星。
它支持在一个或多个具有不同分配策略的工作器上运行,并且可以仅使用几行代码就可以在CPU或GPU上运行。 它的API提供了一个与Estimators和Keras一起使用的简单入口点。 请参考以获取一些代码示例。 支持所有类型的...
爬虫简单说来包括两个步骤:获得网页文本、过滤得到数据。 1、获得html文本。 python在获取html方面十分方便,寥寥数行代码就可以实现我们需要的功能。 复制代码 代码如下: def getHtml(url): page = urllib.urlopen...
本资源包含SAP-PPDS 自开发启发式的配置和开发两部分内容,完整版示例代码,代码也有详细的注释,超级稀有!
本文提出一个两阶段的启发式算法来求解MDVRPTW. 该算法首先通过基于聚集度的启发式分类算法将MDVRPTW简化为多个VRPTW; 然后采用蚁群算法对每个VRPTW进行求解. 为了提高蚁群算法的效率, 提出了两个改进策略: 交叉...
入口坐标和出口坐标分别为(startx,starty)和(endx,eny),每一个坐标点有两种可能:0或1,其中0表示该位置允许通过,1表示该位置不允许通过。以寻路问题为例实现A*算法的求解程序,设计两种不同的估价函数。
结构中有两个成员,其一为指向下一个人的指针,以构成环形的链;其二为该人是否被扔下海的标记,为1表示还在船上。从第一个人开始对还未扔下海的人进行计数,每数到9时,将结构中的标记改为0,表示该人已被扔下海了...
我们将首先在程序中增加一个计数器,然后通过不断地修改,从而使程序的代码变得越来越短,但程序的功能却会变得越来越强,最终的结果是只需要几行代码就可以使算法的运行时间达到平均水平。在第三节将对前面的技术...
每次入职体验均由两个主要部分组成-后台页面和内容页面。 背景包括静态背景图像/视频,页面控件和跳过按钮。 内容页面由四部分组成:图像/图标,标题,正文和操作按钮。 通过创建OnboardingContentViewController...
有三个.cpp文件,代码是我亲手写的,都可以运行,这个代码包含有3种方式避免死锁的方法,一个是允许四个哲学家同时进餐,第二个是一下子就拿两根筷子,否则不拿,第三个就是奇数哲学家先拿左边的筷子,偶数哲学家拿...
论文研究-车辆调度问题的分派启发式算法.pdf, 对有时间窗的车辆调度问题进行了分析,提出了以分派为基础的启发式算法.算法中讨论了如何完成任务所需要的车辆数,定义了...
Ohos_ZBLibrary 架构,提供一套开发标准(View... 用 [Entry, V>],两个变量的 Model/JavaBean 再也不用写了; 用 [BaseHttpListActivity],几行代码搞定 HTTP 请求列表 加载和缓存; 一行搞定 View 属性,一键统一配置UI
质数和合数是数学中的两个基本概念,两位数以内的质数和合数还比较好区分,位数一多,很多人就不太好判断了,但如果借用工具的话,这个问题就比较容易得到解答了。Scratch是一款深受青少儿喜爱的编程入门工具,虽然...
基于Matlab代码,完美复现了两阶段鲁棒优化文献《微电网两阶段鲁棒优化经济调度方法_刘一欣》。跟目前流传的版本不同,本人硕士方向为微网两阶段鲁棒优化调度,纯原创!内容构建了微网两阶段鲁棒调度模型,建立了min...
在这个仿真实验中解决了自定义的20个城市的TSP问题,在设定合适参数后每次的运行中都能得到一个比较理想的结果。 Main.m文件是程序入口。 Data_file.m文件设置自定义的城市数据。 Swapcities.m文件中包含随机交换两...
《C和C++代码精粹》基于作者备受好评的C/C++ User Journal...《C和C++代码精粹》可以帮助有一定经验的C和C++程序员深入学习这两种密切相关的语言,对书中代码的参悟和应用,可以帮助他们从根本上提高使用程序的效率。