【问题标题】:Moving: what does it take?搬家:需要什么?
【发布时间】:2012-05-16 09:25:53
【问题描述】:

std::string的移动赋值运算符(VC11中)如何使用?

我希望它会自动使用,因为分配后不再需要 v。 在这种情况下需要 std::move 吗?如果是这样,我还不如使用非 C++11 交换。

#include <string>

struct user_t
{
    void set_name(std::string v)
    {
        name_ = v;
        // swap(name_, v);
        // name_ = std::move(v);
    }

    std::string name_;
};

int main()
{
    user_t u;
    u.set_name("Olaf");
    return 0;
}

【问题讨论】:

  • 我在任何地方都没有看到移动构造函数?
  • @NicolBolas:这与这个问题无关。他正在询问是否要移动班上的std::string 成员。他不是想搬家。
  • @MooingDuck:哦。好吧,那就别管了。不过,对于近距离投票,我真的无能为力……
  • @Corbin:嗯? std::string 应该有移动赋值,不是吗?
  • @XTF:Corbin(和另外两个人)误解了你的问题,并认为你试图移动user_t。请澄清问题。

标签: c++ visual-c++ c++11 visual-studio-2012 move-semantics


【解决方案1】:

我希望它会自动使用,因为分配后不再需要 v。在这种情况下需要 std::move 吗?

必须为左值明确声明运动,除非它们是从函数(按值)返回的。

这可以防止意外移动某些东西。记住:运动是一种破坏性的行为;你不希望它发生。

另外,如果name_ = v; 的语义根据这是否是函数中的最后一行而改变,那会很奇怪。毕竟,这是完全合法的代码:

name_ = v;
v[0] = 5; //Assuming v has at least one character.

为什么第一行有时要执行复制,而有时要执行移动?

如果是这样,我还不如使用非 C++11 交换。

您可以随心所欲,但std::move 的意图更明显。我们知道它的含义以及您正在用它做什么。

【讨论】:

  • Move 基本上是对文案的优化。因此,如果编译器检测到这样做是安全的,那么它会自动执行此优化。
  • @XTF:这会让我们很难知道代码将要做什么,它是否会调用复制或移动构造函数。 Elision 已经做了类似的事情,但它只发生在非常特定的情况下。即使这样,当没有发生省略时,您仍然可以准确地知道调用了哪个构造函数。
  • 这里需要记住的规则是,任何有名字的东西都是左值,只能对右值进行隐式移动。
【解决方案2】:

接受的答案是一个很好的答案(我已经赞成)。但我想更详细地解决这个问题:

我的问题的核心是:为什么它不选择移动分配 自动运营商?编译器知道 v 在 任务,不是吗?还是 C++11 不需要编译器 这么聪明?

在设计移动语义时考虑了这种可能性。在极端情况下,您可能希望编译器进行一些静态分析并尽可能从对象中移出:

void set_name(std::string v)
{
    name_ = v;  // move from v if it can be proven that some_event is false?
    if (some_event)
       f(v);
}

最终要求编译器进行这种分析是非常棘手的。一些编译器可能能够证明,而其他编译器可能不能。从而导致代码不是真正可移植的。

好的,那么一些没有 if 语句的更简单的情况呢?

void foo()
{
    X x;
    Y y(x);
    X x2 = x;  // last use?  move?
}

嗯,很难知道y.~Y() 是否会注意到x 已被移出。总的来说:

void foo()
{
    X x;
    // ...
    // lots of code here
    // ...
    X x2 = x;  // last use?  move?
}

编译器很难对此进行分析以了解在复制构造到x2 之后是否真的不再使用x

所以最初的“移动”提案给出了一个非常简单且非常保守的隐式移动规则:

左值只能在复制的情况下隐式移动 省略已经是允许的。

例如:

#include <cassert>

struct X
{
    int i_;
    X() : i_(1) {}
    ~X() {i_ = 0;}
};

struct Y
{
    X* x_;
    Y() : x_(0) {}
    ~Y() {assert(x_ != 0); assert(x_->i_ != 0);}
};

X foo(bool some_test)
{
    Y y;
    X x;
    if (some_test)
    {
        X x2;
        return x2;
    }
    y.x_ = &x;
    return x;
}

int main()
{
    X x = foo(false);
}

这里,根据 C++98/03 规则,这个程序可能会或可能不会断言,这取决于 return x 处的复制省略是否发生。如果确实发生了,程序运行良好。如果没有发生,则程序断言。

因此推断:当允许 RVO 时,我们已经处于无法保证 x 值的区域。所以我们应该能够利用这个余地并从x 转移。风险看起来很小,而收益看起来巨大。这不仅意味着通过简单的重新编译,许多现有程序会变得更快,而且还意味着我们现在可以从工厂函数返回“仅移动”类型。这是一个非常大的收益风险比。

在标准化过程的后期,我们有点贪心,还说在返回按值参数时会发生隐式移动(并且类型与返回类型匹配)。这里的好处似乎也相对较大,尽管代码破坏的机会稍大一些,因为这不是 RVO 合法(或现在)合法的情况。但是我没有针对这种情况的破解代码的演示。

因此,最终,您的核心问题的答案是移动语义的原始设计在破坏现有代码方面采取了非常保守的路线。如果不是这样,它肯定会在委员会中被击落。在这个过程的后期,有一些变化使设计更具侵略性。但到了这个时候,核心提案已经在标准中根深蒂固,得到了多数(但不是一致)的支持。

【讨论】:

    【解决方案3】:

    在您的示例中,set_name 按值获取字符串。然而,在set_name 内部,v 是一个左值。让我们分别处理这些情况:

    user_t u;
    std::string str("Olaf");    // Creates string by copying a char const*.
    u.set_name(std::move(str)); // Moves string.
    

    set_name 中调用std::string 的赋值运算符, 这会导致不必要的副本。但也有rvalue overloadoperator=, 在您的情况下这更有意义:

    void set_name(std::string v)
    {
        name_ = std::move(v);
    }
    

    这样,唯一发生的复制是字符串构造 (std::string("Olaf"))。

    【讨论】:

    • 在这里移动一个按值传递的参数是没有意义的。您正在避免一份副本,但您已经将初始字符串复制到按值传递的参数中。
    • @fontanini:这是一个常见的选项,因为它在一个函数中同时传递右值和左值都相当高效。
    • 扩展@MooingDuck 的回答:通过仅使用值语义,由 调用者 来决定是移动还是复制。
    • 哈,错过了“str”参数上的std::move ;)
    • @XTf:在set_name 内部,v 是一个左值。您需要通过std::move 显式将其“转换”为右值以选择适当的重载。
    猜你喜欢
    • 2013-08-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-04-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多