若在下一步中对象被破坏,为什么不自动移动呢

Why not auto move if object is destroyed in next step?

本文关键字:为什么不 移动 下一步 对象      更新时间:2023-10-16

如果函数返回如下值:

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));
}

为什么?在我看来,这是不必要的复杂,因为右值和移动语义很难理解,尤其是对于初学者来说。因此,如果您在标准情况下不必在意,但无论如何都可以从移动语义中受益(比如使用返回值和临时值),那就太好了。同样令人烦恼的是,必须查看类定义来发现一个类是否启用了移动并从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也很冗长,降低了可读性。

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

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被move操作置于无效状态,但Bar持有对它的引用,并将在它被销毁时尝试使用它,这种情况发生在foo调用之后。

如果make_bar是在外部程序集中定义的(例如DLL/so),则编译器在编译Bar b = make_bar(a);时无法判断b是否持有对a的引用。因此,即使foo(a)a的最后一个用法,也不意味着使用移动语义是安全的,因为由于以前的指令,其他一些对象可能持有对a的引用。

只有才能通过查看所调用函数的规范来知道是否可以使用移动语义。

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

这一切都是对"Destroyed"的定义的总结?std::string没有自毁的特殊效果,而是释放隐藏在里面的char数组。

如果我的析构函数做了什么特别的事情呢?例如,做一些重要的日志记录?然后通过简单地"移动它,因为它不再需要了",我错过了析构函数可能会做的一些特殊行为。

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

(请注意,这在很大程度上取决于您试图传递的内容,在这种情况下,还取决于STL实现。在编译时所有代码都可用的情况下,编译器可能会确定复制构造函数和析构函数都是纯的,并对其进行优化。)

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

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

现在对于第二个片段,您的字符串a是一个左值。Move语义仅适用于通过返回临时的、未命名的对象或强制转换左值而获得的右值引用。后者正是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());