【问题标题】:What lasts after using std::move c++11使用 std::move c++11 后会持续什么
【发布时间】:2014-01-17 23:13:27
【问题描述】:

在可能是类中的字段的变量中使用 std::move 之后:

class A {
  public:
    vector<string>&& stealVector() {
      return std::move(myVector);
    }
    void recreateMyVector() {
    }
  private:
    vector<string> myVector;        
};

如何重新创建矢量,就像一个清晰的? std::move 之后 myVector 中还剩下什么?

【问题讨论】:

  • 请不要返回右值引用...
  • 是的,这不适合使用右值引用。如果你想让它把向量移出类,你应该按值返回。
  • 恭喜,现在调用stealVector,您的班级处于未指定状态。
  • @Guillaume07 - 这是不好的做法。您几乎总是希望按值返回/传递以使用移动语义。这个问题的答案解释了原因:Is returning by r-value reference more efficient。如果您正在使用引用折叠进行一些模板元编程,您可能还可以更好地利用“通过 r 值引用返回”,尽管对于大多数用户来说这似乎是一个晦涩的案例。

标签: c++ c++11


【解决方案1】:

常见的说法是,已“移出”的变量处于有效但未指定的状态。这意味着可以销毁分配给变量,但没有别的。

(我相信Stepanov 称其为“部分形成”,这是一个很好的术语。)


需要明确的是,这不是一个严格的规则;相反,它是关于如何考虑移动的指南:从某物移动后,您不应该再使用原始对象。任何试图对原始对象做一些不平凡的事情(除了分配给它或破坏它)都应该仔细考虑并证明是合理的。

但是,在每种特定情况下,可能存在对移出对象有意义的其他操作,您可能希望利用这些操作。例如:

  • 标准库容器描述了其操作的先决条件;没有先决条件的操作很好。想到的唯一有用的是clear(),也许还有swap()(但更喜欢赋值而不是交换)。还有其他没有前置条件的操作,比如size(),但是按照上面的推理,你不应该有任何业务查询你刚才说你不想要的对象的大小更多。

  • unique_ptr&lt;T, D&gt; 保证在被移出后为空,您可以在有条件地取得所有权的情况下利用它:

    std::unique_ptr<T> resource(new T);
    std::vector<std::function<int(std::unique_ptr<T> &)> handlers = /* ... */;
    
    for (auto const & f : handlers)
    {
        int result = f(resource);
        if (!resource) { return result; }
    }
    

    处理程序如下所示:

    int foo_handler(std::unique_ptr<T> & p)
    { 
        if (some_condition))
        {
            another_container.remember(std::move(p));
            return another_container.state();
        }
        return 0;
    }
    

    通常可以让处理程序返回某种其他类型的状态,指示它是否从唯一指针获取所有权,但由于标准实际上保证从唯一指针移动将其保留为空,我们可以利用它在唯一指针本身中传输该信息。

【讨论】:

  • “但没有别的” 这不太正确:您可以使用所有没有前置条件的成员函数(通常像 const 观察者)。见stackoverflow.com/questions/7027523/…
  • @DyP:嗯,任何特定的类当然可以做出任何数量的额外保证。我说的是在代码中遇到move 时应该考虑的一般方式。
  • 据我了解,“有效但未指定的状态”确实意味着:它不是无效状态,因此您仍然可以使用该对象,您只需不知道它处于 what 状态(例如:一组有效状态的哪个状态)。因此,任何不需要状态是特定集合之一的(成员)函数都可以用于移出对象。
  • @DyP:再说一次,您无法从公共接口判断成员函数是否访问状态,而这很少得到合同保证。因此,尽管您的推理是合理的,但我不确定它是否广泛适用。同样,这是您应该考虑move的方式,而不是一组精确的操作限制。
  • 除了 destroyassign to 之外,你还可以对没有前置条件的std::vector 做任何其他事情,例如myVector.clear() . myVector 是“完全形成”,而不是“部分形成”。你只是不知道它处于什么完全形成的状态,除非你用不需要前置条件的查询(例如myVector.size()myVector.empty())来检查它。或者,如果您通过不需要前置条件的操作(例如myVector.clear()myVector = otherVectormyVector.~vector&lt;T&gt;())将其强制为指定状态。
【解决方案2】:

将成员向量移动到局部向量,清除成员,按值返回局部。

std::vector<string> stealVector() {
    auto ret = std::move(myVector);
    myVector.clear();
    return ret;
}

【讨论】:

  • 这通过示例展示了如何在从它移动后清除向量,并显示了比返回参考更好的选择,但没有回答关于从它移动后向量中剩下什么的问题.
  • +1 无论如何,用于显示返回参考的更好选择。
  • 移动后是否允许调用clear()?这不会导致未定义的行为吗?我会将其替换为 myVector = std::vector&lt;string&gt;();
  • @martinus:不,没有未定义的行为。 clear() 没有前置条件。
【解决方案3】:

std::move 之后 myVector 中还剩下什么?

std::move 不动,它只是一个演员表。在调用stealVector() 后,myVector 可能完好无损;请参阅下面示例代码中第一个 a.show() 的输出。 (是的,这是一个愚蠢但有效的代码。)

如果myVector 的内脏真的被盗了(参见示例代码中的b = a.stealVector();),它将处于有效但未指定的状态。然而,它必须是可分配和可破坏的;如果是std::vector,您也可以安全地调用clear()swap()。你真的不应该对向量的状态做任何其他假设。

如何重新创建矢量,就像一个清晰的?

一种选择是简单地调用clear()。那么你肯定知道它的状态。


示例代码:

#include <initializer_list>
#include <iostream>
#include <string>
#include <vector>
using namespace std;

class A {
  public:
    A(initializer_list<string> il) : myVector(il) { }
    void show() {
      if (myVector.empty())
        cout << "(empty)";
      for (const string& s : myVector)
        cout << s << "  ";
      cout << endl;
    }
    vector<string>&& stealVector() {
      return std::move(myVector);
    }
  private:
    vector<string> myVector;        
};

int main() {
  A a({"a", "b", "c"});
  a.stealVector();
  a.show();
  vector<string> b{"1", "2", "3"};
  b = a.stealVector();
  a.show();
}

这会在我的机器上打印以下内容:

a b c
(空)

【讨论】:

    【解决方案4】:

    由于到目前为止我觉得 Stepanov 在答案中被歪曲了,所以让我快速概述一下我自己的观点:

    对于std 类型(以及那些),标准规定移出对象处于著名的“有效但未指定”状态。特别是,std 类型都没有使用 Stepanov 的部分形成状态,包括我在内的一些人认为这是一个错误。

    对于您的自己的类型,您应该争取默认构造函数以及移动的源对象以建立部分形成状态,Stepanov 在 Elements of Programming (2009) 中定义作为一种状态,其中唯一有效的操作是销毁和分配新值。特别是,Partially-Formed State 不需要表示对象的有效值,也不需要遵守正常的类不变量。

    与流行的看法相反,这并不是什么新鲜事。 Partially-Formed State 自 C/C++ 诞生之日起就存在:

    int i; // i is Partially-Formed: only going out of scope and
           // assignment are allowed, and compilers understand this!
    

    这对用户来说实际上意味着永远不要假设您可以对已移动的对象做更多的事情,而不是销毁它或为它分配一个新值,当然,除非文档说明您可以做更多,这对于容器来说通常是可能的,它们通常可以自然而有效地建立空状态。

    对于班级作者,这意味着您有两种选择:

    首先,您要避免像 STL 那样的部分形成状态。但是对于具有远程状态的类,例如一个 pimpl'ed 类,这意味着要表示一个有效值,要么接受 nullptr 作为 pImpl 的有效值,提示您在公共 API 级别定义 nullptr pImpl 的含义, 包括在所有成员函数中检查nullptr

    或者您需要为移动的(和默认构造的)对象分配一个新的pImpl,这当然不是任何注重性能的 C++ 程序员会做的事情。然而,一个注重性能的 C++ 程序员也不希望在他的代码中乱扔nullptr 检查,只是为了支持一个不平凡的使用移出对象的次要用例。

    这将我们带到了第二种选择:拥抱部分形成的国家。这意味着,您接受 nullptr pImpl,但仅适用于默认构造和移出的对象。 nullptr pImpl 代表部分形成状态,其中只允许销毁和分配另一个值。这意味着只有 dtor 和赋值运算符需要能够处理nullptr pImpl,而所有其他成员都可以假定一个有效的pImpl。这还有另一个好处:您的默认 ctor 和移动运算符都可以是 noexcept,这对于在 std::vector 中使用很重要(因此在重新分配时使用移动而不是副本)。

    例如Pen类:

    class Pen {
        struct Private;
        Private *pImpl = nullptr;
    public:
        Pen() noexcept = default;
        Pen(Pen &&other) noexcept : pImpl{std::exchange(other.pImpl, {})} {}
        Pen(const Pen &other) : pImpl{new Private{*other.pImpl}} {} // assumes valid `other`
        Pen &operator=(Pen &&other) noexcept {
            Pen(std::move(other)).swap(*this);
            return *this;
        }
        Pen &operator=(const Pen &other) {
            Pen(other).swap(*this);
            return *this;
        }
        void swap(Pen &other) noexcept {
            using std::swap;
            swap(pImpl, other.pImpl);
        }
        int width() const { return pImpl->width; }
        // ...
    };
    

    【讨论】:

    • 每天我都更加坚信接受部分形成的状态。现在我认为如果允许包含的值处于部分形成状态,则应该允许 containers 处于部分形成状态。例如std::vector v(20) 应该包含处于部分形成状态的对象(如果T t 处于部分形成状态)。否则我们总是可以做“std::vector v(20, {});”。在实践中std::vector v(20) 应该调用std::uninitialized_default_constructstd::vector v(20, {}) 应该调用std::uninitialized_value_construct。你怎么看?
    猜你喜欢
    • 2013-01-07
    • 1970-01-01
    • 1970-01-01
    • 2023-03-22
    • 2019-04-23
    • 2014-09-30
    • 2015-09-06
    相关资源
    最近更新 更多