关于指针的拥有权移交问题。

james_razor 2003-06-30 02:34:28
keyword: pointer ownership, const_case, copy ctro, operator = .

auto_ptr是一个大家都熟悉的模板类,它能实现较为方便的内存管理功能,而且是异常安全的。在某个项目中,我们需要实现一个能自动管理内存的类,大家知道auto_ptr是不能用在数组上的,而且,在对象的内存管理上,它也只限于确保对象内存能被释放,仅此而已。所以,我们需要自己写一个类。命名为SmartBuffer。

类的功能其实很简单,构造函数(ctro),拷贝构造函数(copy ctro),解构函数(dtro),赋值函数(operator =)大都参照auto_ptr编写,仅对数组处理作了修改。然后增加了一些额外的public函数,如往缓冲区添加数据(AddData),从缓冲区取数据(GetData),然后写了些对应的private函数供内部使用,如检查缓冲区是否够用,如不够则重新分配内存的函数(CheckBuffer)。
但在使用时发现有一个问题,如果我这么申明和定义拷贝构造函数:
SmartBuffer::SmartBuffer(SmartBuffer &sb)
{
m_pBuffer = sb.m_pBuffer;
sb = NULL;
}
和一个使用此类的函数
SmartBuffer Func()
{
SmartBuffer sb;
// 一些操作,产生的数据放在sb中
return sb;
}
以及对这个函数的使用:
SmartBuffer sb = Func();
但是,在Visual studio .net下上面的这行编译不通过。

要说明的是,我把vc中“配置属性”->“C/C++”->“语言”->“禁用语言扩展”给选上了,因为我要考虑到移植的问题,微软的扩展也许会带来一些问题。而如果这个选项关闭。问题就不存在了。

编译错误的提示是“无法把SmartBuffer转化为SmartBuffer”,各位也许会感到奇怪,怎么类自己都不能转化为自己?我当然也是感到非常的困惑,几经周折之后,我发现这个copy ctro有问题,它接受的是SmartBuffer &sb!而上面这个Func中,由于sb是一个临时变量,所以,需要调用的拷贝构造函数是SmartBuffer(const SmartBuffer &sb)。而const变量不能被修改。也就意味着我无法转移拥有权!为了对这个方案进行修补,我采取了较为野蛮的手段,就是直接用const_cast除去sb.m_pBuffer的const属性,也就达到了我的目的。
SmartBuffer::SmartBuffer(const SmartBuffer &sb)
{
m_pBuffer = sb.m_pBuffer;
char *p = const_cast <char *> (sb.m_pBuffer);
p = NULL;
}
然后,对operator = 我也作了相同的修改,防止出现类似的问题。
但问题是,这样的方案有没有缺陷?有没有更好的实现方法?望各位不吝赐教。

顺便说一下:STL中的auto_ptr,也是不能写出如:
auto_ptr <myclass> Func()
{
auto_ptr <myclass> ap(new myclass)
return ap;
}
这样的函数。
auto_ptr <myclass> p = Func();
像这样调用的。
打开微软的语言扩展是可以编译通过的,但我不清楚微软到底做了哪方面的扩展,而且从语言的移植性上考虑,最好使用标准C++,尽量避免局限于某一种编译器上。
...全文
59 42 打赏 收藏 转发到动态 举报
写回复
用AI写文章
42 条回复
切换为时间正序
请发表友善的回复…
发表回复
ejiue 2003-09-08
  • 打赏
  • 举报
回复
mark
james_razor 2003-07-11
  • 打赏
  • 举报
回复
谢谢各位,我认为差不多了,给分,我级别低,无法给更多的分,所以只能考虑三位主要的回答者,其他的给连接和资料的大虾,我也表示感谢。如果认为问题还需要继续讨论,我会开新贴。
glasswing 2003-07-10
  • 打赏
  • 举报
回复
mark
ajoo 2003-07-10
  • 打赏
  • 举报
回复
其实james_razor (蹬三轮的) ,
要完成你的要求,也不一定要自己写,scope guard应该也可以达到你的要求。
我也写过一个类似的:

/*
* Provides transaction control.
* a Tx object is neither cloneable nor assignable.
* It supports commit and rollback operations.
* destructor will rollback the transaction if it was not committed or rolled back.
* Normal process of using Tx is:
* 1. start the transaction by making the changes
* 2. create a Tx object; Initialize the object with the undo functor object.
* 3. call commit() to commit the transaction or call rollback or the dtor to rollback the transaction.
*/
template<typename R>
class Tx{
private:
typedef Tx<R> Self;
R r;
bool committed;
bool rolledback;
Tx(const Self& other){throw "copy constructor disallowed";}
Tx& operator=(const Tx& other){throw "operator= disallowed";}
void init(){
committed = false;
rolledback = false;
}
bool can_rollback()const{
return !committed&&!rolledback;
}
public:
/*
* Constructs a Tx object with the undo functor object.
*/
explicit Tx(R _r=R()):r(_r){
init();
}
/*
* Constructs a Tx object with the resource handle.
* The constructor will use the resource handle to construct a temporary undo functor object.
*/
template<typename _R>
explicit Tx(_R _r):r(R(_r)){
init();
}
/*
* tells if the transaction is already rolled back. On initialization, this is false.
*/
bool is_rolled_back()const{
return rolledback;
}
/*
* tells if the transaction is already committed. On initialization, this is false.
*/
bool is_committed()const{
return committed;
}
/*
* roll back the transaction by calling the undo functor object.
* if the transaction was rolled back or committed before, rollback() is no-op.
*/
void rollback(){
if(can_rollback()){
r();
rolledback=true;
}
}
/*
* commit the transaction.
* if the transaction was rolled back or committed before, commit() is no-op.
* This method returns the reference to the functor object.
* commit() guarantees that no exception is ever thrown out of it.
*/
R& commit()throw(){
if(!rolledback)
committed=true;
return r;
}
/*
* destructor. If the transaction was neither rolled back nor committed, it will be rolled back.
*/
~Tx(){
if(can_rollback()){
try{
r();
}
catch(...){}
}
}
};


template<typename T>
class Deletor{
T* const ptr;
public:
explicit Deletor(T* p):ptr(p){}
void operator()()const{
delete ptr;
}
T* get()const{
return ptr;
}
};
template<typename T>
class ArrDeletor{
T* const arr;
public:
explicit ArrDeletor(T* a):arr(a){}
void operator()()const{
delete [] arr;
}
T* get()const{return arr;}
};


typedef Tx<ArrDeletor<char> > TxString;

ajoo 2003-07-10
  • 打赏
  • 举报
回复
为什么临时变量必须不能赋值?允许赋值有什么问题呢?允许改变临时变量有什么问题呢?事实上明显很多情况下都需要改变临时变量的值嘛
waterain 2003-07-10
  • 打赏
  • 举报
回复
mark!
fierygnu 2003-07-10
  • 打赏
  • 举报
回复
关于mutable和const_cast的问题不再争论了,我们互相都无法说服。

临时变量的引用必须是const的问题我的例子是可以说明问题的,“因为临时变量是不能赋值的,所以只有被声明为const,才能传递其引用。”这就是根本原因。
ajoo 2003-07-10
  • 打赏
  • 举报
回复
以前没有用过auto_ptr_ref, 刚才看了看代码。发现它的原理和我的方案其实是一样的。都是明确转移所有权。
只不过它把release()之后的指针用一个auto_ptr_ref封装了一下。这样做的唯一的好处就是省得程序员语法上明确调用release()(或者在我的解决方法中得give())。但是代价是引入了额外的中间层,代码变得更复杂,而且依赖于template conversion operator。
所做的事却和简单地调用release()/give()没有任何区别。

虽然见仁见智,但我个人认为这样的设计单纯追求语法上的美观,为了隐藏一个没有必要隐藏的函数调用,实际上复杂化了语义,得不偿失。

综上所述,我反对auto_ptr_ref。
ajoo 2003-07-10
  • 打赏
  • 举报
回复
编译器有责任保证const_cast的语义的正确的。你贴的这个贴子和这个例子不同。这里面,m_buffer本来不是const的,SmartBuffer也不是,所以不会由于优化造成问题。

当然,const_cast不好是肯定的。我只是说,它还比mutable好。
mutable是写程序的人和编译器一致了,但骗了读程序的人,风格极坏。
不错,"重要的是怎么用它"。但这里正好是不应该用它的场合。

临时变量和右值是c++的最让人头疼的问题之一。
你举这个例子也说明不了为什么临时变量必须为const, 因为你这里是一个literal, 它和临时变量还是不同的。literal没有地址,而临时变量是有地址的。
ajoo 2003-07-09
  • 打赏
  • 举报
回复
const_cast只是在const这个保护层上开了一个小洞,而mutable就是干脆把这个const扔掉。
如果说const_cast是设计上的补救的话,mutable就干脆是一个坏的设计。

一个好的设计原则是:do what you say. be explicit.

如果你不得不做一些不好的事情的话,把它明确化, 坦坦荡荡。
const_cast是难看,但是它让你明确地感觉到设计上的缺陷。人家读你的代码可以对缺陷一目了然。
mutable从语法上避免了const_cast, 但是它不是解决缺陷,而只是隐藏掩盖缺陷,甚至扩大了缺陷,就象隐瞒SARS一样。典型的hack!

关于临时变量为什么不能是non-const, 我也不大清楚。个人感觉这个规定好像不合理。Andrei的scope guard也是因为这个原因用了mutable这个hack.

哪位明白的给解释解释?拜托别背书名。

simouse 2003-07-09
  • 打赏
  • 举报
回复
up
topikachu 2003-07-09
  • 打赏
  • 举报
回复
还是推荐auto_ptr_ref的思路 :) 引入一个中间层
http://www.csdn.net/develop/Article/18/18137.shtm
james_razor 2003-07-09
  • 打赏
  • 举报
回复
我明白ajoo的意思,从继承的角度来看,const_cast要比mutable来的好,因为它不会把不好的设计带到子类中。不过我觉得如果是自己的代码,mutable要比const_cast好一些,mutable是设计上预知修改的可能性,而const_cast象是设计上的补救措施。

不过我觉得C++在临时变量的这种限制让人觉得不解,以C/C++这种功能强大的语言来说,给与程序员的限制是很少的。像指针,类型转换,不检查数组边界,内存的分配释放等,无一不是以安全性换取程序的灵活性,而进一步换取程序的效率。而这个临时变量必须为const的限制,我看不出有什么潜在的大缺陷,而各位提供的解答,也只是说哪个方法更好,没有提出重大的缺陷。我想,也许取消这个限制会更好,因为我看不出这个问题的缺陷会比数组越界引起的问题更大。

不过,ajoo严谨的设计风格确实让人钦佩,欢迎继续讨论。
ajoo 2003-07-09
  • 打赏
  • 举报
回复
const对象是什么意义?不就是说:不会改变对象的状态吗?这个“状态”难道只是只公共变量吗?如果私有变量没必要被const管辖,都可以随便是mutable, 那还要const方法做什么?只管公共变量吗?谁的类的成员都是公共的?
ajoo 2003-07-09
  • 打赏
  • 举报
回复
"孩子留在家里我自己照看着呢,外人只有通过我允许的方法访问,包括我的后代们,因为我已经声明了,它是我私有的。而且如果我不高兴说,谁也不知道我有个孩子的,呵呵。"

其实,如果事情逼到不得已得份上,这也确实是一途的。

但是,这个方法并非是根本地解决了问题,而只是把坏影响局限到这个类内部而已。也就是说,这个类内部的代码将受到const无法管辖m_buffer的影响,变得不那么安全。更严重的是,如果你在某一个const方法内部改变了m_buffer的值,编译器不会报错。类的客户会被欺骗。明明调用了一个const方法,可是调用完了一看,这个对象的状态竟然神秘地改变了!!!晕!

按照这个“私有”的逻辑,我同样也可以在类代吗内部做const_cast, 一样可以解决问题嘛。而且,那样,我可以把const_cast的坏影响局限在更小的一个函数范围内。james_razor的原来的方法就是只局限在一个函数范围内的。所以我说它比mutable还要好。



fierygnu 2003-07-09
  • 打赏
  • 举报
回复
至于临时变量(的引用)必须是const的原因(原来的问题少了引用)举例如下:
double& dr = 1; / / error: lvalue needed
const double& cdr = 1; / / ok
因为临时变量是不能赋值的,所以只有被声明为const,才能传递其引用。
fierygnu 2003-07-09
  • 打赏
  • 举报
回复
const_cast有很大的问题,会因为编译器的实现不同引入很多意想不到的问题,参考:
http://expert.csdn.net/Expert/TopicView1.asp?id=2001588

mutable在程序员和编译器之间保持了一致,是最不容易产生问题的。不存在“只是隐藏掩盖缺陷,甚至扩大了缺陷”的问题。重要的是你怎么用它。
james_razor 2003-07-08
  • 打赏
  • 举报
回复
ajoo和fierygnu,两位说得都有道理,记下了,多谢。

在第二回贴中,topikachu提到auto_ptr_ref,我看了那篇文章,看意思是说auto_ptr借助auto_ptr_ref就能以:
auto_ptr <int> ap = auto_ptr <int> (new int(8));
的形式使用,我试了一下,不行啊,怎么回事儿?(关闭微软扩展)
james_razor 2003-07-08
  • 打赏
  • 举报
回复
没办法,无法提前了,up一下。
james_razor 2003-07-07
  • 打赏
  • 举报
回复
嗬嗬,ajoo的观点比较激烈哦,我以为,如果说它不好,要看它会不会带来什么问题。追求程序的优雅当然是每个优秀的程序员都想做到的,但有时候不得不进行一些妥协,而且我认为事实上mutable看上去要比const_cast准确一些,const_cast是在某处(此例中在构造函数中)暂时修改了m_pBuffer的属性,造成了语义的不一致性,而mutable在一开始就告诉编译器,这个m_pBuffer是一个会被修改的成员变量,即使包含它的类是const属性。

但回过来看看ajoo的意见,也有道理,我想这是事情的两个方面,难以取舍。

加上liao2001和ajoo提出的方法,现在已有四种方法了,真是太感谢各位了。
加载更多回复(22)

69,336

社区成员

发帖
与我相关
我的任务
社区描述
C语言相关问题讨论
社区管理员
  • C语言
  • 花神庙码农
  • 架构师李肯
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧