如果在下一步中销毁对象,为什么不自动移动?

时间:2015-03-18 09:39:28

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

如果函数返回如下值:

std::string foo() {
  std::string ret {"Test"};
  return ret;
}

允许编译器移动ret,因为它不再使用。这不适用于这样的情况:

void foo (std::string str) {
  // do sth. with str
}

int main() {
  std::string a {"Test"};
  foo(a);
}

虽然显然不再需要a,因为它在下一步中被销毁,你必须这样做:

int main() {
  std::string a {"Test"};
  foo(std::move(a));
}

为什么呢?在我看来,这是不必要的复杂,因为rvalues和移动语义很难理解,特别是对于初学者。因此,如果你不必关心标准情况,但无论如何都要从移动语义中获益(比如返回值和临时值),这将是很棒的。同样令人讨厌的是必须查看类定义以发现一个类是否已启用移动并且完全受益std::move(或者使用std::move,希望它有时会有所帮助。如果您使用现有代码,它也容易出错:

int main() {
  std::string a {"Test"};
  foo(std::move(a));

  // [...] 100 lines of code
  // new line:
  foo(a); // Ups!
}

如果不再使用对象,编译器会更好地了解。 std::move到处都是冗长的,并且会降低可读性。

4 个答案:

答案 0 :(得分:7)

在给定点之后不会使用对象并不明显。 例如,查看代码的以下变体:

struct Bar {
  ~Bar() { std::cout << str.size() << std::endl; }
  std::string& str;
}

Bar make_bar(std::string& str) {
  return Bar{ str };
}

void foo (std::string str) {
  // do sth. with str
}

int main() {
  std::string a {"Test"};
  Bar b = make_bar(a);
  foo(std::move(a));
}

此代码会中断,因为字符串a被移动操作置于无效状态,但是Bar持有对它的引用,并且会在它被销毁时尝试使用它,这发生在 foo电话后。

如果在外部程序集中定义make_bar(例如DLL / so),编译器在编译Bar b = make_bar(a);时无法告知b是否持有对{{1}的引用} 或不。因此,即使afoo(a)的最后一次使用,也不意味着使用移动语义是安全的,因为某些其他对象可能会持有对a的引用之前的指示。

只有才能知道您是否可以使用移动语义,方法是查看您调用的函数的规范。

另一方面,您始终可以在a情况下使用移动语义,因为该对象无论如何都将超出范围,这意味着任何持有对它的引用的对象都将导致未定义的行为,而不管移动语义。 顺便说一句,你甚至不需要移动语义,因为复制省略。

答案 1 :(得分:1)

它总结了你所定义的&#34;被毁坏的&#34;? std :: string对自毁没有特殊效果,但是释放隐藏在里面的char数组。

如果我的析构函数有什么特别之处怎么办?例如 - 做一些重要的日志记录?然后简单地移动它,因为它不再需要了#34;我想念析构函数可能会做的一些特殊行为。

答案 2 :(得分:0)

因为编译器不能进行改变程序行为的优化,除非标准允许。在某些情况下允许return优化,但方法调用不允许进行此优化。通过改变行为,它会跳过调用复制构造函数和析构函数,它们可能有副作用(它们不需要是纯粹的),但是通过跳过它们,这些副作用不会发生,因此行为会被改变。 / p>

(请注意,这在很大程度上取决于您尝试传递的内容,在本例中是STL实现。如果编译时所有代码都可用,编译器可能会确定复制构造函数和析构函数都是纯粹的并且优化他们出去了。)

答案 3 :(得分:0)

虽然允许编译器在第一个片段中移动ret,但它也可以执行复制/移动省略并将其直接构造到调用者的堆栈中。 这就是为什么不建议写这样的函数的原因:

std::string foo() {
    auto ret = std::string("Test");

    return std::move(ret);
}

现在,对于第二个片段,您的字符串a是左值。移动语义仅适用于rvalue-references,它通过返回临时的,未命名的对象或转换左值来获得。后者正是std :: move所做的。

std::string GetString();

auto s = GetString();

// s is a lvalue, use std::move to cast it to rvalue-ref to force move semantics
foo(s);

// GetString returns a temporary object, which is a rvalue-ref and move semantics apply automatically
foo(GetString());