首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 开发语言 > C++ >

关于 operator=(), swap 和 栈对象 的 错误安全有关问题

2013-07-04 
关于 operator(), swap 和 栈对象 的 异常安全问题在重写 类的 operator()时, 一般会注意异常安全问题,

关于 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);}
}

热点排行