欢迎来到关于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;
- }
这个构造将被 SFINAE 掉,除非 _alloc 本身具有无抛出的转移构造函数,且这个成员会从 rhs 的相应成员转移过来,从而被隐式地初始化。
不幸的是,对于C++0x中属性的应有作用一直存在一些分歧,所以我们还不知道委员会会接受怎样的语法,但至少我们认为原则上我们已经理解了问题何在,以及如何解决它。
后续
好了,感谢你的阅读;今天就到此为止。下一篇我们将讨论完美转发,还有,我们也没有忘记还欠你一个关于C++03的转移模拟的调查。