右值系列之五:异常安全的转移

原文来自:http://cpp-next.com/archive/2009/10/exceptionally-moving/

欢迎来到关于C++中的高效值类型的系列文章中的第五篇。在上一篇中,我们停留在对转移赋值最优实现的不断找寻中。今天,我们将要找到一条穿过这个“转移城市(Move City)”的道路,在这里,最普通的类型都可能有令人惊讶的冲突。

在前面的文章中,我们看到,通过提供”转移的权限”,可以让同一段代码同时用于可转移类型和不可转移类型,并在可能的时候尽量利用转移优化。这种“在可能的时候转移,在必须的时候复制”的方法,对于代码优化是很有用的,也兼容于那些没有转移构造函数的旧类型。不过,对于提供强异常保证的操作来说,却增加了新的负担。

强异常保证,强异常要求

实现强异常保证要求将某个操作的所有步骤分为两类:

  1. 有可能抛出异常但不包含任何不可逆改变的操作
  2. 可能包含不可逆改变但不会抛出异常的操作


强异常保证依赖于对各步操作的分类

如果我们将所有动作分入这两类,且保证任何第1类的动作都在第2类动作之前发生,就没我们什么事了。在C++03中有一个典型例子,当 vector::reserve() 需要为新元素分配内存时:

  1. void reserve(size_type n)  
  2. {  
  3.     if (n > this->capacity())  
  4.     {  
  5.         pointer new_begin = this->allocate( n );  
  6.         size_type s = this->size(), i = 0;  
  7.         try  
  8.         {  
  9.             // copy to new storage: can throw; doesn't modify *this  
  10.             for (;i < s; ++i)  
  11.                  new ((void*)(new_begin + i)) value_type( (*this)[i] );  
  12.         }  
  13.         catch(...)  
  14.         {  
  15.             while (i > 0)                 // clean up new elements  
  16.                (new_begin + --i)->~value_type();  
  17.    
  18.             this->deallocate( new_begin );    // release storage  
  19.             throw;  
  20.         }  
  21.         // -------- irreversible mutation starts here -----------  
  22.         this->deallocate( this->begin_ );  
  23.         this->begin_ = new_begin;  
  24.         this->end_ = new_begin + s;  
  25.         this->cap_ = new_begin + n;  
  26.     }  
  27. }  

如果是在支持转移操作的实现中,我们需要在 try 块中加上一个对 std::move 的显式调用,将循环改为:

  1. for (;i < s; ++i)  
  2.      new ((void*)(new_begin + i)) value_type( std::move( (*this)[i] ) );  

在这点变化中,有趣的是,如果 value_type 是支持转移的,那么在循环中会改写 *this (从左值进行显式的转移请求,是一种逻辑上有改写的操作)。

现在,如果转移操作会抛出异常,这个循环就会产生不可逆转的变化,因为要回滚一个部分完成的循环是需要更多的转移操作。因此,要在 value_type 支持转移的情况下保持强异常保证,它的转移构造函数必须是无抛出的。


  可能有抛出的转移操作不能做到无潜在再次抛出的回滚

结果

C++0x 标准草案中基本上是反对可抛出的转移构造函数的,我们建议你遵守此规则。不过,转移构造函数必须无抛出这条规则并不总是那么容易遵守的。以 std::pair<std::string,UserType> 为例,其中 UserType 是带有可抛出复制构造函数的类型。在 C++03 中,这个类型是没有问题的,可以用在 std::vector 中。但是在 C++0x 中,std::string 带有转移构造函数,std::pair 同样也有:

  1. template <class FirstType, class SecondType>  
  2. pair<FirstType,SecondType>::pair(pair&& x)  
  3.   : first(std::move(x.first))  
  4.   , second(std::move(x.second))  
  5. {}  

这里就有问题了。second 的类型是 UserType,它没有转移构造函数,这意味着 second 的构造是一次(有可能抛出的)复制构造,而不是转移构造。所以,pair<std::string, UserType> 给出的是一个可抛出的转移构造函数,它不能再用于 std::vector 中而不破坏强异常保证了。

今天,这意味着我们需要一些类似于以下代码的东西来令 pair 可用。

  1. template   
  2. pair(pair&& rhs  
  3.   , typename enable_if<                 // Undocumented optional  
  4.         mpl::and_<                      // argument, not part of the  
  5.             boost::has_nothrow_move // public interface of pair.  
  6.           , boost::has_nothrow_move  
  7.         >  
  8.      >::type* = 0  
  9. )  
  10.   : first(std::move(rhs.first)),  
  11.     second(std::move(rhs.second))  
  12. {};  

通过使用 enable_if,可以令到这个构造函数“消失”,除非 has_nothrow_move 对于 T1 和 T2 均为 true。

我们知道,没有办法检测是否存在一个转移构造函数,更不要说它是否无抛出了,因此,在我们得到新的语言特性之前,boost::has_nothrow_move 都是补救的方法之一,它对于用户自定义类型返回 false,除非你对它进行了特化。所以,在你编写一个转移构造函数时,应该对这个 trait 进行特化。例如,如果我们为 std::vector 和 std::pair 增加了转移构造函数,我们还应该加上:

  1. namespace boost  
  2. {  
  3.     // All vectors have a (nothrow) move constructor  
  4.     template <class T, class A>  
  5.     struct has_nothrow_move<std::vector<T,A> > : true_type {};  
  6.    
  7.     // A pair has a (nothrow) move constructor iff both its  
  8.     // members do as well.  
  9.     template <class First, class Second>  
  10.     struct has_nothrow_move<std::pair<First,Second> >  
  11.       : mpl::and_<  
  12.            boost::has_nothrow_move<First>  
  13.          , boost::has_nothrow_move<Second>  
  14.         > {};  
  15. }  

我们承认这很不好看。C++委员会还在讨论如何解决这个问题的细节,不过以下一些事情都已经获得普通同意:

  • 我们不能由于静静地放弃了强异常保证而破坏现有的代码。
  • 可以通过在适当的时候生成缺省的转移构造函数——正如 Bjarne Stroustrup 在 N2904 中所建议的——减小这个问题。这可以修复 pair 以及所有类似类型的问题,同时通过增加生成的转移优化,还可以“免费”提升一些代码的速度。
  • 还是有些类型需要我们“手工”来处理。

“有问题的类型”

归为有问题的类型通常都带有我们想要转移的子对象——已提供了安全实现——和其它一些我们需要“其它操作”的子对象。std::vector 就是一个例子,它带有一个分配器,其复制构造函数有可能会抛出异常:

  1. vector(vector&& rhs)  
  2.   : _alloc( std::move(rhs._alloc) )  
  3.   , _begin( rhs._begin )  
  4.   , _end( rhs._end )  
  5.   , _cap( rhs._cap )  
  6. {  
  7.     // "something else"  
  8.     rhs._begin = rhs._end = rhs._cap = 0;  
  9. }  

一个简单的成员式转移,例如在 N2904 中所说的缺省生成的那个,在这里将不具有正确的语义。尤其是,它不会把 rhs 的 _begin, _end 以及 _cap 置零。但是,如果 _alloc 不具有一个无抛出的转移构造函数,那么在第2行中就只能进行复制。如果该复制可以有抛出异常,那么 vector 提供的就是可抛出的转移构造函数。

对于语言设计者来说,挑战是如何避免要求用户两次提供相同的信息。既要在转移构造函数的签名中指明成员的类型是可转移的(前面的 pair 转移构造函数中的第5、6行),又要在成员初始化列表中再真正对成员进行转移(第10、11行)。目前正在讨论的一个可能性是使用一个新的属性语法,令到 vector 的转移构造函数可以这样写:

  1. vector(vector&& rhs) [[moves(_alloc)]]  
  2.   : _begin( rhs._begin )  
  3.   , _end( rhs._end )  
  4.   , _cap( rhs._cap )  
  5. {  
  6.     rhs._begin = rhs._end = rhs._cap = 0;  
  7. }  

这个构造将被 SFINAE 掉,除非 _alloc 本身具有无抛出的转移构造函数,且这个成员会从 rhs 的相应成员转移过来,从而被隐式地初始化。

不幸的是,对于C++0x中属性的应有作用一直存在一些分歧,所以我们还不知道委员会会接受怎样的语法,但至少我们认为原则上我们已经理解了问题何在,以及如何解决它。

后续

好了,感谢你的阅读;今天就到此为止。下一篇我们将讨论完美转发,还有,我们也没有忘记还欠你一个关于C++03的转移模拟的调查。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值