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

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

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

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

强异常保证,强异常要求


实现强异常保证要求将某个操作的所有步骤分为两类:
有可能抛出异常但不包含任何不可逆改变的操作
可能包含不可逆改变但不会抛出异常的操作


如果我们将所有动作分入这两类,且保证任何第1类的动作都在第2类动作之前发生,就没我们什么事了。在C++03中有一个典型例子,当 vector::reserve() 需要为新元素分配内存时:
void reserve(size_type n)
{
    if (n > this->capacity())
    {
        pointer new_begin = this->allocate( n );
        size_type s = this->size(), i = 0;
        try
        {
            // copy to new storage: can throw; doesn't modify *this
            for (;i < s; ++i)
                 new ((void*)(new_begin + i)) value_type( (*this)[i] );
        }
        catch(...)
        {
            while (i > 0)                 // clean up new elements
               (new_begin + --i)->~value_type();
 
            this->deallocate( new_begin );    // release storage
            throw;
        }
        // -------- irreversible mutation starts here -----------
        this->deallocate( this->begin_ );
        this->begin_ = new_begin;
        this->end_ = new_begin + s;
        this->cap_ = new_begin + n;
    }
}
如果是在支持转移操作的实现中,我们需要在 try 块中加上一个对 std::move 的显式调用,将循环改为:
for (;i < s; ++i)
     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 同样也有:
template <class FirstType, class SecondType>
pair<FirstType,SecondType>::pair(pair&& x)
  : first(std::move(x.first))
  , second(std::move(x.second))
{}
这里就有问题了。second 的类型是 UserType,它没有转移构造函数,这意味着 second 的构造是一次(有可能抛出的)复制构造,而不是转移构造。所以,pair<std::string, UserType> 给出的是一个可抛出的转移构造函数,它不能再用于 std::vector 中而不破坏强异常保证了。
今天,这意味着我们需要一些类似于以下代码的东西来令 pair 可用。
template 
pair(pair&& rhs
  , typename enable_if<                 // Undocumented optional
        mpl::and_<                      // argument, not part of the
            boost::has_nothrow_move // public interface of pair.
          , boost::has_nothrow_move
        >
     >::type* = 0
)
  : first(std::move(rhs.first)),
    second(std::move(rhs.second))
{};
通过使用 enable_if,可以令到这个构造函数“消失”,除非 has_nothrow_move 对于 T1 和 T2 均为 true。
我们知道,没有办法检测是否存在一个转移构造函数,更不要说它是否无抛出了,因此,在我们得到新的语言特性之前,boost::has_nothrow_move 都是补救的方法之一,它对于用户自定义类型返回 false,除非你对它进行了特化。所以,在你编写一个转移构造函数时,应该对这个 trait 进行特化。例如,如果我们为 std::vector 和 std::pair 增加了转移构造函数,我们还应该加上:
namespace boost
{
    // All vectors have a (nothrow) move constructor
    template <class T, class A>
    struct has_nothrow_move<std::vector<T,A> > : true_type {};
 
    // A pair has a (nothrow) move constructor iff both its
    // members do as well.
    template <class First, class Second>
    struct has_nothrow_move<std::pair<First,Second> >
      : mpl::and_<
           boost::has_nothrow_move<First>
         , boost::has_nothrow_move<Second>
        > {};
}
我们承认这很不好看。C++委员会还在讨论如何解决这个问题的细节,不过以下一些事情都已经获得普通同意:
我们不能由于静静地放弃了强异常保证而破坏现有的代码。
可以通过在适当的时候生成缺省的转移构造函数——正如 Bjarne Stroustrup 在 N2904 中所建议的——减小这个问题。这可以修复 pair 以及所有类似类型的问题,同时通过增加生成的转移优化,还可以“免费”提升一些代码的速度。
还是有些类型需要我们“手工”来处理。


“有问题的类型”


归为有问题的类型通常都带有我们想要转移的子对象——已提供了安全实现——和其它一些我们需要“其它操作”的子对象。std::vector 就是一个例子,它带有一个分配器,其复制构造函数有可能会抛出异常:
vector(vector&& rhs)
  : _alloc( std::move(rhs._alloc) )
  , _begin( rhs._begin )
  , _end( rhs._end )
  , _cap( rhs._cap )
{
    // "something else"
    rhs._begin = rhs._end = rhs._cap = 0;
}
一个简单的成员式转移,例如在N2904中所说的缺省生成的那个,在这里将不具有正确的语义。尤其是,它不会把 rhs 的 _begin, _end 以及 _cap 置零。但是,如果 _alloc 不具有一个无抛出的转移构造函数,那么在第2行中就只能进行复制。如果该复制可以有抛出异常,那么 vector 提供的就是可抛出的转移构造函数。
对于语言设计者来说,挑战是如何避免要求用户两次提供相同的信息。既要在转移构造函数的签名中指明成员的类型是可转移的(前面的 pair 转移构造函数中的第5、6行),又要在成员初始化列表中再真正对成员进行转移(第10、11行)。目前正在讨论的一个可能性是使用一个新的属性语法,令到 vector 的转移构造函数可以这样写:
vector(vector&& rhs) [[moves(_alloc)]]
  : _begin( rhs._begin )
  , _end( rhs._end )
  , _cap( rhs._cap )
{
    rhs._begin = rhs._end = rhs._cap = 0;
}
这个构造将被 SAFINE 掉,除非 _alloc 本身具有无抛出的转移构造函数,且这个成员会从 rhs 的相应成员转移过来,从而被隐式地初始化。
不幸的是,对于C++0x中属性的应有作用一直存在一些分歧,所以我们还不知道委员会会接受怎样的语法,但至少我们认为原则上我们已经理解了问题何在,以及如何解决它。
后续


好了,感谢你的阅读;今天就到此为止。下一篇我们将讨论完美转发,还有,我们也没有忘记还欠你一个关于C++03的转移模拟的调查。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值