当向量增长时如何强制使用移动语义?

111

我有一个特定类别 A 的对象 std::vector。该类别是非平凡的,具有定义的复制构造函数 移动构造函数。

std::vector<A>  myvec;
如果我使用myvec.push_back(a)向vector中添加A对象,它的大小会增长,并使用复制构造函数A( const A&)来实例化向量中元素的新副本。
我能否以某种方式强制使用类A的移动构造函数?

6
您可以通过使用一个具有移动感知的向量实现来实现此功能。 - K-ballo
2
你能否更具体地说明如何实现这个? - Bertwim van Beest
1
你可以使用一个支持移动语义的向量实现。听起来你的标准库实现(顺便问一下,它是哪个版本?)不支持移动语义。你可以尝试使用 Boost 中支持移动语义的容器。 - K-ballo
1
我使用的是gcc 4.5.1,它具备移动感知功能。 - Bertwim van Beest
在我的代码中,将复制构造函数设为私有的方式是可行的,即使移动构造函数没有显式地声明“noexcept”。 - Arne
3个回答

154

你需要告诉 C++(特别是 std::vector)你的移动构造函数和析构函数不会抛出异常,使用 noexcept。然后当向量增长时,移动构造函数就会被调用。

这是声明和实现一个被 std::vector 所认可的移动构造函数的方法:

A(A && rhs) noexcept { 
  std::cout << "i am the move constr" <<std::endl;
  ... some code doing the move ...  
  m_value=std::move(rhs.m_value) ; // etc...
}
如果构造函数不是noexceptstd::vector无法使用它,因为这样它无法确保标准所要求的异常保证。如需了解更多有关标准的内容,请阅读C++ Move semantics and Exceptions。 感谢 Bo 提示这可能与异常有关。同时请考虑 Kerrek SB 的建议,在可能的情况下使用 emplace_back。它可能会更快(但通常不是),也可以更清晰、更简洁,但也存在一些陷阱(特别是对于非显式构造函数)。 编辑,通常默认值就是您想要的:移动所有可移动的内容,复制其余的内容。要明确请求,请编写:
A(A && rhs) = default;

这样做,你将在可能的情况下获得noexcept:默认的移动构造函数是否被定义为noexcept?

请注意,早期版本的Visual Studio 2015和更早版本不支持此功能,尽管它支持移动语义。


有趣的是,实现如何“知道”value_type的移动构造函数是否为noexcept?也许语言在调用范围也是noexcept函数时限制了函数调用候选集合? - Lightness Races in Orbit
1
@LightnessRacesinOrbit 我认为它只是执行诸如http://en.cppreference.com/w/cpp/types/is_move_constructible之类的操作。只能有一个移动构造函数,因此应该在声明中明确定义。 - Johan Lundberg
@LightnessRacesinOrbit,我后来了解到,实际上没有(标准/有用的)方法可以真正知道是否存在noexcept移动构造函数。如果存在nothrow复制构造函数,则is_nothrow_move_constructible将为true。我不知道是否存在昂贵的nothrow复制构造函数的真实情况,因此它是否真的很重要并不清楚。 - Johan Lundberg
1
@Johan,我找到问题了。我在错误的push_back()调用上使用了std::move()。有时候你会为了寻找问题而努力地看着它,却没有看到明显的错误就在你面前。然后就到了午餐时间,我忘记删除我的评论了。 - AlastairG
但是如果移动构造函数和复制构造函数都可能抛出异常怎么办? - einpoklum
显示剩余2条评论

19

有趣的是,gcc 4.7.2的向量(vector)只有在移动构造函数和析构函数都是noexcept时才使用移动构造函数。以下是一个简单的示例:

struct foo {
    foo() {}
    foo( const foo & ) noexcept { std::cout << "copy\n"; }
    foo( foo && ) noexcept { std::cout << "move\n"; }
    ~foo() noexcept {}
};

int main() {
    std::vector< foo > v;
    for ( int i = 0; i < 3; ++i ) v.emplace_back();
}

这将输出预期结果:

move
move
move

然而,当我从~foo()中移除noexcept时,结果是不同的:

copy
copy
copy

我想这也回答了这个问题


在我看来,其他答案似乎只谈到了移动构造函数,而没有提到析构函数必须是noexcept的。 - Nikola Benes
嗯,本应该没问题的,但事实证明,在gcc 4.7.2中并非如此。因此,这个问题实际上是特定于gcc的。不过在gcc 4.8.0中应该已经修复了。请参见相关stackoverflow问题 - Nikola Benes

-1

看起来, (对于 C++17 和早期版本) 强制 std::vector 在重新分配时使用移动语义的唯一方法是删除复制构造函数 :). 以此方式,它将在编译时使用您的移动构造函数或尝试死亡 :).

有许多规则,std::vector 在重新分配时不得使用移动构造函数,但没有关于它必须使用它的规则。

template<class T>
class move_only : public T{
public:
   move_only(){}
   move_only(const move_only&) = delete;
   move_only(move_only&&) noexcept {};
   ~move_only() noexcept {};

   using T::T;   
};

直播

或者

template<class T>
struct move_only{
   T value;

   template<class Arg, class ...Args, typename = std::enable_if_t<
            !std::is_same_v<move_only<T>&&, Arg >
            && !std::is_same_v<const move_only<T>&, Arg >
    >>
   move_only(Arg&& arg, Args&&... args)
      :value(std::forward<Arg>(arg), std::forward<Args>(args)...)
   {}

   move_only(){}
   move_only(const move_only&) = delete;   
   move_only(move_only&& other) noexcept : value(std::move(other.value)) {};    
   ~move_only() noexcept {};   
};

实时代码

你的 T 类必须具有 noexcept 移动构造函数/赋值运算符和 noexcept 析构函数。否则会导致编译错误。

std::vector<move_only<MyClass>> vec;

1
不需要删除复制构造函数。如果移动构造函数是noexcept,它将被使用。 - balki

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接