【问题标题】:std::move( ) calls copy-ctor in the absence of a move-ctor. Why and how to prevent it?std::move( ) 在没有 move-ctor 的情况下调用 copy-ctor。为什么以及如何预防?
【发布时间】:2013-08-18 07:41:05
【问题描述】:

我想知道是否有一种安全编程实践可以提醒编码人员注意这种微妙的行为,或者更好的是,首先避免它。

struct A 的用户可能没有意识到没有移动构造函数。在他们尝试调用不存在的 ctor 时,他们既没有收到编译器警告,也没有收到任何运行时指示,而是调用了复制 ctor。

下面的答案解释了发生的转换,但我不认为这是一件好事。如果缺少将 const 引用 作为参数的构造函数,则会出现编译时错误,而不仅仅是解析为 non-const 引用 版本。那么,当类中没有实现移动语义时,为什么尝试使用移动语义不会导致编译时错误?

有没有办法通过一些编译时选项来避免这种行为,或者至少有一种方法可以在运行时检测到它?

搬家后如果他们可以assert(source is null),他们已经预料到了这个问题,但很多问题都是如此。

例如,当:

struct A {
    A() {...}
    A(A &a) {...}
    A(A const & A) {...}
};

构造如下:

A a1;
A a2 = std::move(a1);  //calls const copy (but how would I know?)

这会导致调用复制 ctor 的 const 版本。现在两个对象可能有一个指向单个资源的指针,而其中一个可能很快就会调用它的析构函数。

【问题讨论】:

  • “现在两个对象可能有一个指向单个资源的指针,而其中一个可能很快就会调用它的析构函数。”那么这是你程序中的一个错误。修复它。
  • 如果你的类设计得好,就创建一个默认的。 A(A &&) = default
  • @Arbalest:你的整个问题都是基于复制而不是移动是一个错误。您可以通过复制您的课程并对其进行测试并遵守三规则来检测它。搬家与此无关。
  • @Arbalest 我想简短的回答是你不能。其他评论者遇到的问题是为什么您认为这是一个问题。这就是结构 A 的设计方式。意思是“我可以被复制,但不能被感动”。
  • 只要把移动想象成复制的优化。当编译器跳过其他优化时,您不会收到警告;你为什么会在这里期待一个?

标签: c++ c++11 move-semantics


【解决方案1】:

由于 std::move 返回一个右值,它可以转换为一个 const ref,这就是为什么复制 ctor 被静默调用的原因。您可以通过多种方式解决此问题。

  1. 简单的方法,如果您的类没有动态分配,只需像这样使用默认 mctor。
    A(A &&) = default;
  2. 去掉 cctor 中的 const,这不是一个好主意,但它不会编译比

  3. 只要实现你自己的移动构造函数,你为什么要移动到一个没有mctor的对象


这有点离题,但您也可以限定成员函数只使用这样的可修改左值。

int func()&;

如果您正在处理遗留代码,这里是 move constructable 的编译时检查

【讨论】:

  • 这个问题没有问如何解决这个问题。它询问如何检测/避免问题,因为没有编译时或运行时指示存在问题。您的所有三个建议都假定理论程序员“控制”struct A 的源代码并注意到问题。由于我声明缺少预期的功能,结果是另一个被调用,我知道我可以提供缺少的功能。
  • @Arbalest 你不知道 3/5 的规则,那么实现这些东西是你的责任。如果您没有移动构造函数,您是否希望编译器警告您,因为这是可能的
  • 那么让我试着说得更清楚些。想象一下,程序员正在使用他/她无法控制的遗留 API,并错误地尝试使用 API 不支持的 移动语义。代码在调用时编译并运行,没有异常。所以是的,你如何让编译器警告你没有移动构造函数。
  • @Arbalest 我可以给你一个编译时检查是否存在 mctor 是否可以接受
  • @Arbalest:就像我上面说的,移动与与错误无关。遗留 API 中的错误是其复制构造函数不起作用。移动变成副本是完全正常的(考虑:T x = get_T();)。所以你在遗留 API 中发现了一个错误:报告它并解决它。将其包装到您自己的禁止复制构造和分配的类中。
【解决方案2】:

没有一种安全的编程习惯可以提醒这种事情,因为在std::move 之后进行复制是非常普遍和正常的。添加警告会导致<algorithm> 中的几乎所有内容都开始触发警告,我们需要一个全新的函数来使用完全 就像std::move 现在所做的那样。如果没有移动构造函数,很多算法都依赖于复制,否则就依赖于移动,这就是std::move 的用途。充其量你应该争论std::force_move的存在。

其次,这完全没有必要。采用您展示的代码的修改版本。

void legacy_api(A a1) {
   A a2 = std::move(a1);
   ...

你说它巧妙地使用了昂贵的副本而不是微妙的举动是一个问题。我不同意,它需要的是一个新的对象实例,可能会破坏以前的实例。如果代码需要另一个实例,那么它需要另一个实例。它是否破坏了先前的应该完全无关紧要。如果代码在不需要另一个实例的情况下正在移动,那么很明显,遗留 API 是混合编写的,并且上述警告无济于事。我想不出任何理由一个函数会需要移动但没有副本,这样的事情没有任何目的。

最后,“现在两个对象可能有一个指向单个资源的指针,而其中一个可能很快就会调用它的析构函数。”不,如果发生这种情况,则意味着A 的复制构造函数有一个错误,句号。修复代码中的错误,问题就会消失。就像魔术一样!

【讨论】:

  • 您认为std::move 与不存在的std::force_move 不同的想法抓住了问题的关键,并且几乎回答了问题。因为它是std::move 更像是std::move_if_you_can。我没有要解决的错误或要进行的优化。我想知道(你回答)是(对于任何情况 - 我不在乎)是否有类似编译器开关的东西警告移动请求导致了副本。如果没有别的,它可以告诉你哪些类应该在以后实现移动语义。
  • @Arbalest:这样的事情会为std::vector<int> 产生数百个警告,所以毫无意义。实际上,最好的选择是使用拥有指针成员移动任何东西。如果您真的迷路了,请使用分析器。
  • @Arbalest 来自What is move semantics?,大约是std::move强调我的):“这个名字有点不幸,因为std::move 只是将左值转换为右值; 它自己移动任何东西。它只是启用移动。也许它应该被命名为std::cast_to_rvaluestd::enable_move,但是我们现在被这个名字困住了。”所以我认为想要一个“force_move”或谈论“move_if_you_can”是没有意义的。 [...]
  • [...] 现在,对于您的问题,您似乎需要警告将右值引用隐式转换为左值引用到常量(又名“const 左值引用”) ,例如“warning: implicit conversion from 'A&&' to 'const A&'”。不幸的是,正如 Mooing Duck 所说,它会对所有 STL 发出警告。此外,给定A a;,我认为在const A& r = std::move(a); 中没有从A&&const A& 的实际转换,而是使用表达式std::move(a) 初始化r(产生@987654346 类型的右值@),将 r 绑定到 a(参见 N3337 中的 8.5.3 [dcl.init.ref])。
  • @gx_ 感谢您的意见。后来我确实遇到了同样的帖子(和其他材料)。我可以看到它本质上是rvalue_cast<>(),我也可以看到不这么称呼它的意义。我还看到 move 的意思是“如果可以的话,请移动”。我想我被困在 const_cast 的常用用法中缺乏对称性。使用大量 const 正确性的调用程序/程序员立即知道某些 API 没有适当的构造函数 - 不支持意图。然后他们放入 const_cast 使其工作,未来的维护者也会看到它。
【解决方案3】:

因为 std::move 返回对右值的引用(如在 A&& 中),它不能转换为对左值的引用 (A&),但可以转换为对左值的 const 引用 (const A&)。

int x = 5。这里 5 是一个右值,永远不能绑定到一个左值,在您的示例中也是如此,因为您使用了 std::move(a1)

【讨论】:

    【解决方案4】:

    如果您是要移动的类的所有者,但该类没有移动构造函数,因为您不想移动它,您可以这样写:

    class A
    {
    public:
       // Other constuctors
    
       A(A&&) = delete;
    };
    

    因此,当您尝试移动 A 时,如果我没有记错,则会引发编译器错误。如果你希望对象总是被移动,你当然应该编写自己的移动构造函数,或者启用默认的移动构造函数:

     A(A&&) = default;
    

    如果您不是该类的所有者,我认为没有直接的方法可以避免调用复制构造函数。强迫它的可能成语如下:

    template<bool b, typename T>
    using enabling = typename std::enable_if<b, T>::type;
    
    template<typename T>
    constexpr bool movable()
    {
       return std::is_move_constructible<T>::value;
    }
    
    template<typename T>
    enabling<movable<T>(), T&&> movify(T&& t)
    {
        return std::move(t);
    }
    
    A a1;
    A a2 = movify(a1);
    

    应该可以。我没有对此进行测试,但我认为您已经掌握了这个想法。

    【讨论】:

      猜你喜欢
      • 2019-11-09
      • 2021-08-09
      • 2019-04-24
      • 2022-11-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多