读书人

关于 operator=(), swap 和 栈对象 的

发布时间: 2013-07-04 11:45:32 作者: rapoo

关于 operator=(), swap 和 栈对象 的 异常安全问题
在重写 类的 operator=()时, 一般会注意异常安全问题,使用 swap, RAII 手法进行设计, 如:


class Foo
{
Foo();
Foo( const Foo& other);
~Foo();

void MySwap( Foo& other);

Foo& operator=( const Foo& other)
{
//... 自赋值等检测

Foo temp( other);
MySwap( temp); // 问题在这里

return *this;
}
};




暂且不管具体的 MySwap, Foo构造函数的实现, 在 operator=() 函数内的
MySwap( temp);
进行 当前对象的数据内容 与 临时栈对象 temp 的数据内容 进行交换,this的数据存放于temp的栈上, 当退出该函数后, this的数据不就指向 在无效的 栈上了吗? 否的话, 又为什么?
[解决办法]
引用:
在重写 类的 operator=()时, 一般会注意异常安全问题,使用 swap, RAII 手法进行设计, 如:

class Foo
{
Foo();
Foo( const Foo& other);
~Foo();

void MySwap( Foo& other);

Foo& operator=( const Foo& other)
{
//... 自赋值等检测

Foo temp( other);
MySwap( temp); // 问题在这里

return *this;
}
};




暂且不管具体的 MySwap, Foo构造函数的实现, 在 operator=() 函数内的
MySwap( temp);
进行 当前对象的数据内容 与 临时栈对象 temp 的数据内容 进行交换,this的数据存放于temp的栈上, 当退出该函数后, this的数据不就指向 在无效的 栈上了吗? 否的话, 又为什么?



MySwap并非互换彼此的地址,而是互换彼此的实现,所以是不存在是否在栈上的问题的。

另外,对于copy and swap手法,一般不进行自我赋值检测了,因为这种情况毕竟很少,而且即使遇到,swap也不会造成大问题,大不了就是损失点性能罢了,无需为少数情况惩罚多数情况。

operator=还可以改为下面这样,更简洁:

Foo& operator=( const Foo other)
{
MySwap( other );
return *this;
}

[解决办法]
引用:
进行 当前对象的数据内容 与 临时栈对象 temp 的数据内容 进行交换,this的数据存放于temp的栈上, 当退出该函数后, this的数据不就指向 在无效的 栈上了吗? 否的话, 又为什么?


this 的数据不在 operator= 函数的栈上! this 的数据就是你的对象, 你的对象在哪里定义的, this 的数据就在哪里.
如果你是把 this 理解成 operator= 的一个参数, 那么也只是 this 这个指针在栈上, 而不是 this 指向的对象数据在栈上.
实际上, 大多数 this 都不通过栈参数传递, 而是通过寄存器来传参的.
[解决办法]
只要swap是无代价的(比如只是交换,象string、vector那样,相比vector复制的过程,指针交换的代价已经可以简略了),效率问题并不需要过多考虑,因为这种实现主要就是释放旧资源并分配新资源:

T temp(other);//分配资源,复制对象
swap(temp);//无代价
return * this;//返回引用,无代价
}//析构temp,释放资源

当然对比在拷贝赋值中不需要重新分配资源的实现,确实在时间效率上有所损失,但一是这种情况并不常见, 更重要的是,这样的实现更加健壮,如我前面所说,与拷贝构造的一致性,面对异常的操作完整性(或者赋值成功、或者没有改变),面对这些好处,一些的时间效率损失应该是可以接受的。
[解决办法]
引用:
iostream的问题很复杂,它有派生,却没有虚析构,所以我们不能new一个ofstream然后保存在一个ostream指针里面……它的派生关系只是为了能把一个ofstream对象传递给一个ostream&参数,而在拥有这个流对象(也就是负责释放这个对象)的代码中,必须保留着它的原始类型。iostream通常以值对象的方式存在,但它却又不能赋值……
iostream这个自C++诞生就存在的东西其实有很多不合理的地方。

为啥?你的意思是说下面的代码不能编译吗。

#include <fstream>
#include <iostream>
int main ()
{
std::ostream* const os = new std::ofstream("test.txt");
delete static_cast<std::ofstream*>(os);
}

[解决办法]
引用:
Quote: 引用:

iostream的问题很复杂,它有派生,却没有虚析构,所以我们不能new一个ofstream然后保存在一个ostream指针里面……它的派生关系只是为了能把一个ofstream对象传递给一个ostream&参数,而在拥有这个流对象(也就是负责释放这个对象)的代码中,必须保留着它的原始类型。iostream通常以值对象的方式存在,但它却又不能赋值……
iostream这个自C++诞生就存在的东西其实有很多不合理的地方。


为啥?你的意思是说下面的代码不能编译吗。

#include <fstream>
#include <iostream>
int main ()
{
std::ostream* const os = new std::ofstream("test.txt");
delete static_cast<std::ofstream*>(os);
}


这个代码当然可以编译。但是我的某个类为了使用RAII技术而设计下面这个类:

class ostream_ptr
{
public:
ostream_ptr(ostream * os)
: my_os(os)
{
}
~XXX()
{
//我现在应该如何释放这个os?
}
private
ostream * my_os;
};



事实上,我们更常使用的是auto_ptr<base_class>或unique_ptr<base_class>,但auto_ptr<ostream>一定不会知道如何正确地释放一个ofstream。

因此,我们用ostream这一类没有虚析构但又有派生关系的类的对象指针的时候,一定要“谁分配谁释放”,既然如此,我为什么不干脆使用值对象呢?

此外,ostream不允许拷贝赋值操作符。

不能把各种ostream以基类指针的方式传递给其它对象,另它不象“真正的对象”;不支持拷贝赋值,另它不象“值对象”,所有说它是一种很特别的存在。
[解决办法]
引用:
进行 当前对象的数据内容 与 临时栈对象 temp 的数据内容 进行交换,this的数据存放于temp的栈上, 当退出该函数后, this的数据不就指向 在无效的 栈上了吗? 否的话, 又为什么?



void MySwap(MyType& other)
{
if(this == &other) return;
//....
auto t = this->a;
this->a = other.a;
other.a = t;
//如果a的类型是T, 则拷贝,没有问题
//如果a的类型是T*, 则交换地址,没有问题
//如果a的类型是T&, 则拷贝,没有问题
//....
}

[解决办法]
引用:
Quote: 引用:

Quote: 引用:

Quote: 引用:

iostream的问题很复杂,它有派生,却没有虚析构,所以我们不能new一个ofstream然后保存在一个ostream指针里面……它的派生关系只是为了能把一个ofstream对象传递给一个ostream&参数,而在拥有这个流对象(也就是负责释放这个对象)的代码中,必须保留着它的原始类型。iostream通常以值对象的方式存在,但它却又不能赋值……
iostream这个自C++诞生就存在的东西其实有很多不合理的地方。

为啥?你的意思是说下面的代码不能编译吗。

#include <fstream>
#include <iostream>
int main ()
{
std::ostream* const os = new std::ofstream("test.txt");
delete static_cast<std::ofstream*>(os);
}


这个代码当然可以编译。但是我的某个类为了使用RAII技术而设计下面这个类:

class ostream_ptr
{
public:


ostream_ptr(ostream * os)
: my_os(os)
{
}
~XXX()
{
//我现在应该如何释放这个os?
}
private
ostream * my_os;
};



事实上,我们更常使用的是auto_ptr<base_class>或unique_ptr<base_class>,但auto_ptr<ostream>一定不会知道如何正确地释放一个ofstream。

因此,我们用ostream这一类没有虚析构但又有派生关系的类的对象指针的时候,一定要“谁分配谁释放”,既然如此,我为什么不干脆使用值对象呢?

此外,ostream不允许拷贝赋值操作符。

不能把各种ostream以基类指针的方式传递给其它对象,另它不象“真正的对象”;不支持拷贝赋值,另它不象“值对象”,所有说它是一种很特别的存在。

你的结论貌似都是基于这一条认识,即 type<base_t> object(new derived_t); 的模式无法正确释放 new derived_t 对象。我想说这是没有根据的,比如至少可以这样做。

#include <iostream>

struct deleter_base_t
{
virtual ~deleter_base_t () { }
};

template <typename T>
struct deleter_t final : deleter_base_t
{
~deleter_t () { delete m_ptr; }
deleter_t (T* ptr) : m_ptr(ptr) { }
private:
T* m_ptr;
};

template <typename static_t>
struct ostream_ptr_impl_t
{
~ostream_ptr_impl_t () { delete m_delete; }

template <typename dynamic_t>
ostream_ptr_impl_t (dynamic_t* os)
: m_delete(new deleter_t<dynamic_t>(os))
{
}

private:
deleter_base_t* m_delete;
};

class ostream_ptr_t
{
public:
template <typename T>
ostream_ptr_t (T* os) : m_os(os)
{
}

private:
ostream_ptr_impl_t<std::ostream> m_os;
};

namespace test
{
struct ostream
{
~ostream () { std::cout << "~ostream ()" << std::endl; }
ostream () { std::cout << " ostream ()" << std::endl; }
};

struct ofstream : ostream
{
~ofstream () { std::cout << "~ofstream ()" << std::endl; }
ofstream () { std::cout << " ofstream ()" << std::endl; }
};
}

int main ()
{
ostream_ptr_t test(new test::ofstream);
}

你后面一系列的推论也因此都不可靠了。
另外,ostream 不支持复制不是讨论的重点,为了避免不必要的跑题,完全可以把 std::ostream/std::ofstream 换成上例中 test::ostream/test::ofstream。


我们一直在讨论你 #9 给出的例子代码中体现的问题,是否有必要只针对具有虚析构函数的类而言(你的观点),还是这种问题存在于任何继承体系中(我的观点),因此没必要只针对前者。


发现一个打字错误,容易引起误解,重新发一下。

#include <iostream>

struct deleter_base_t
{
virtual ~deleter_base_t () { }
};

template <typename T>
struct deleter_t final : deleter_base_t
{
~deleter_t () { delete m_ptr; }
deleter_t (T* ptr) : m_ptr(ptr) { }
private:
T* m_ptr;
};

template <typename static_t>
struct ostream_ptr_impl_t
{
~ostream_ptr_impl_t () { delete m_delete; }

template <typename dynamic_t>
ostream_ptr_impl_t (dynamic_t* os)
: m_delete(new deleter_t<dynamic_t>(os))
{
}

private:
deleter_base_t* m_delete;
};

namespace test
{
struct ostream
{
~ostream () { std::cout << "~test::ostream ()" << std::endl; }
ostream () { std::cout << " test::ostream ()" << std::endl; }
};

struct ofstream : ostream
{
~ofstream () { std::cout << "~test::ofstream ()" << std::endl; }
ofstream () { std::cout << " test::ofstream ()" << std::endl; }
};
}

class ostream_ptr_t
{
public:
template <typename T>
ostream_ptr_t (T* os) : m_os(os)
{
}

private:
ostream_ptr_impl_t<test::ostream> m_os;
};

int main ()
{
{ostream_ptr_t test(new test::ofstream);}
{ostream_ptr_t test(new test::ostream);}
}

读书人网 >C++

热点推荐