【问题标题】:Using an object after std::move doesn't result in a compilation error在 std::move 之后使用对象不会导致编译错误
【发布时间】:2017-08-05 08:39:51
【问题描述】:

在对象上调用std::move后,为什么再使用该对象语言不会导致编译错误?

是不是因为编译器无法检测到这种情况?

【问题讨论】:

  • 因为你可以用它做事,你可以重复使用它。移动使对象处于有效状态。
  • 被移动对象的状态由移动构造函数定义。因此,只要移动构造函数允许,就可以使用它。
  • std::move 只是将对象转换为T&&,它实际上并没有对对象做任何事情。
  • (1) std::move 只是一个没有数据更改的强制转换,因此需要编译器采取任何措施。 (2) 用std::move 的结果做某事在大多数情况下太复杂而无法分析。 (3)如果std::move被实现为rvalue_ref_cast<T>,就不会问很多关于SO的问题
  • move 遵循一个不错的古老 C++ 传统。就像move 不会移动,remove 不会删除,delete 不会删除它的参数:)

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


【解决方案1】:

C++ 语言设计的一般原则是“信任程序员”。在对象成为std::move 的参数之后,我可以想到拒绝对该对象的任何使用的一些问题。

  • 在一般情况下,确定给定用途是否是在调用std::move 之后相当于解决停机问题。 (换句话说,它无法完成。)您必须想出一些规则,以可以静态确定的方式描述“之后”的含义。
  • 一般来说,分配给作为std::move 参数的对象是完全安全的。 (一个特定的类可能会导致一个断言,但这是一个非常奇怪的设计。)
  • 编译器很难判断给定函数是否只会为类的元素分配新值。

【讨论】:

  • 啊,很高兴知道Rust have solved the Halting Problem?
  • 我想知道当移动发生在条件分支中时 Rust 会做什么。它可能是安全的:如果有一个分支发生移动,那么就假定它确实发生了。但这可能会禁止某些有效的程序。
  • 是的,Rust 很安全,即使在编译器知道一个分支永远无法执行的情况下。以下不编译 fn main() { println!("Hello world");让 v1 = vec![1, 2, 3];让 v2 ​​= v1;如果 0 == 1 { v2 = v1; } println!("v1[0]={}", v1[0]); }
  • @KristianR 谢谢。所以 C++ 排除了 Rust 方法,因为“信任程序员”。
【解决方案2】:

请记住,std::move 只不过是对右值引用的强制转换。它本身实际上并没有移动任何东西。此外,该语言仅声明移动的对象处于有效/可破坏状态,但除此之外并没有说明其内容 - 它可能仍然是完整的,它可能不是或 (像 std::unique_ptr 它可能被定义为具有特定内容 (nullptr)) - 这完全取决于移动构造函数/移动分配操作符实现的内容。

因此,访问移动的对象是否安全/有效完全取决于特定的对象。例如,在移动后读取std::unique_ptr,看看它是否是nullptr 非常好——对于其他类型;没那么多。

【讨论】:

    【解决方案3】:

    这就是所谓的“实现质量”问题:对于一个好的编译器或工具链来说,对潜在危险的移动后使用情况发出警告要比语言标准正式禁止它更有意义。一组精确定义的情况。毕竟,某些移动后的使用是合法的(例如 std::unique_ptr 的情况,移动后保证为空)而其他未定义的情况则不容易检测到——一般来说,检测是否对象被移出后使用相当于停止问题。

    移动后可以使用 clang-tidy 检测使用:https://clang.llvm.org/extra/clang-tidy/checks/misc-use-after-move.html

    【讨论】:

    • 据我所知,没有人在移动std::unique_ptr 后使用它。比如...为什么要使用移动的 uniqueptr?
    • "为什么要使用移动的 uniqueptr?"搞错了。
    【解决方案4】:

    这是因为我们经常想要使用被移动的对象。考虑std::swap的默认实现:

    template<typename T> 
    void swap(T& t1, T& t2)
    {
        T temp = std::move(t1);
        t1 = std::move(t2);   // using moved-from t1
        t2 = std::move(temp); // using moved-from t2
    }
    

    您不希望编译器在您每次使用 std::swap 时都向您发出警告。

    【讨论】:

    • 不过,在这种情况下,t1 被分配到后处于良好行为状态。 t2 最后表现得很好(但temp 不是)。当然,编译器可以认为这没问题。
    • 添加到我之前的评论中,具有常规语义的赋值运算符非常合理地调用移动的对象并将其置于明确定义的状态,所以如果我要编写编译器警告,我会跳过上述情况的警告。更困难的情况是还有其他成员函数可以完全定义对象的状态,例如std::unique_ptr::reset。这是标准中的内容,但是如果您编写一个类,则编译器没有注释可以知道“调用此 memfun 会使该类处于完全定义的状态”。所以我猜没有完美的警告。
    • 也就是说,对于典型的语义,在std::move 之后,调用任何const 成员函数之前调用的下一个成员函数几乎总是非const。不能保证非const 成员函数完全定义了对象的状态,但是在移动的对象上调用任何const 成员函数是非常可疑的。
    【解决方案5】:

    对象的移动后状态的性质由实现者定义;在某些情况下,移动对象可能会使其处于完全可用的状态,而在其他情况下,对象可能会变得无用(或者更糟糕的是,使用起来有些危险)。

    虽然我有点同意 - 最好有生成警告的选项,至少,如果在移动后以潜在危险的方式使用对象,我不知道 GCC 或 Clang 的选项引起人们对这种代码异味的注意。

    (事实上,对于勇敢的人来说,这可能会为 a Clang plugin 等提供一个很好的基础!)

    【讨论】:

    • std::move 后对象的状态没有改变。它只是一个花哨的演员。当您转换了改变其状态的原始对象时,它是被调用的函数(移动构造函数移动赋值运算符等)。
    • @RichardCritten 从技术意义上讲,std::move(…) 所做的确实如此——但它允许实现者编写一个移动构造函数(和/或移动赋值运算符)实际上操纵对象的状态。
    猜你喜欢
    • 2019-04-04
    • 2013-09-09
    • 1970-01-01
    • 2013-02-01
    • 1970-01-01
    • 2010-11-07
    • 2011-05-04
    • 2015-09-12
    • 1970-01-01
    相关资源
    最近更新 更多