欢迎来到关于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
template
pair::pair(pair&& x)
: first(std::move(x.first))
, second(std::move(x.second))
{}
这里就有问题了。second 的类型是 UserType,它没有转移构造函数,这意味着 second 的构造是一次(有可能抛出的)复制构造,而不是转移构造。所以,pair
今天,这意味着我们需要一些类似于以下代码的东西来令 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
struct has_nothrow_move > : true_type {};
// A pair has a (nothrow) move constructor iff both its
// members do as well.
template
struct has_nothrow_move >
: mpl::and_<
boost::has_nothrow_move
, boost::has_nothrow_move
> {};
}
我们承认这很不好看。C++委员会还在讨论如何解决这个问题的细节,不过以下一些事情都已经获得普通同意:
归为有问题的类型通常都带有我们想要转移的子对象——已提供了安全实现——和其它一些我们需要“其它操作”的子对象。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的转移模拟的调查。