c++ 为什么我们需要std::move来避免这里的“call to implicitly-deleted copy constructor”错误?

guykilcj  于 2023-06-25  发布在  其他
关注(0)|答案(1)|浏览(523)

代码如下所示。我的想法是,我们有一个P,它是可移动的,但不可复制的。我们有一个OP,它是P的optional Package 器。一个gen_p将生成一个std::pair<int, OP>,其返回值将通过结构化绑定进行“模式匹配”。然后我认为返回值被分解为ipp。我希望pp通过move或RVO返回,至少不是通过copy。

  1. #include <iostream>
  2. #include <vector>
  3. #include <optional>
  4. #include <map>
  5. struct P {
  6. int i;
  7. std::vector<int> v;
  8. P(int ii, std::vector<int>&& vv): i(ii), v(std::move(vv)) {}
  9. P(P&& other) {
  10. i = other.i;
  11. v = std::move(other.v);
  12. }
  13. P & operator=(P && other) {
  14. i = other.i;
  15. v = std::move(other.v);
  16. return *this;
  17. }
  18. P(const P & other) = delete;
  19. P & operator=(const P &) = delete;
  20. };
  21. typedef std::optional<P> OP;
  22. OP move_out_p() {
  23. auto gen_p = [&](){
  24. P pp1 {1, std::vector<int>{2,3,4}};
  25. std::optional<P> op = std::make_optional(std::move(pp1));
  26. return std::make_pair(1, std::move(op));
  27. };
  28. auto&& [i, pp2] = gen_p();
  29. // It compiles with std::move(pp2)
  30. return pp2;
  31. }
  32. static_assert(std::is_move_constructible_v<P>);
  33. void test_move_part() {
  34. auto v = move_out_p();
  35. printf("v size %lu\n", v.value().v.size());
  36. }
  37. int main() {
  38. test_move_part();
  39. }

我会犯错

  1. tm.cpp:87:12: error: call to implicitly-deleted copy constructor of 'OP' (aka 'optional<P>')
  2. return pp2;

我觉得这个错误很奇怪。以下是我的推理:It表示
在这些初始化表达式中,如果实体e的类型是左值引用(这仅在ref限定符是&或如果它是&&并且初始化表达式是左值时发生),则e是左值,否则是xvalue(这有效地执行了一种完美的转发)。
因为我的ref-qualifier是&&,而gen_p()不是左值(它是actually a prvalue),所以在我的情况下,它是一个完美的转发。
我不知道在这个完美的转发案例中我会得到什么。然而,如果pp2是一个左值,那么NRVO将发生。我确信在我的编译器中启用了NRVO,因为我们需要指定-fno-elide-constructors来禁用它。如果pp2是一个xvalue,它将编译。因为std::move(pp)创建了一个xvalue,它编译了。如果pp2是一个纯右值,我不知道,但我发现要求一个纯右值的复制构造函数是没有意义的。所以我认为没有任何情况可以阻止RVO。
此外,如果pp 2是左值,则它应该是RVO。我不明白为什么在这种情况下没有发生RVO。在我们的程序中,我们不返回std::move(pp 2),这是recommended。我不知道我们为什么要打破这个规矩。编译的代码看起来很奇怪。
所以我在这里迷路了?你能给我解释一下这个错误吗?

suzh9iv8

suzh9iv81#

首先是一些术语。
RVO或NRVO是指拷贝省略的特定示例。在C中,复制省略是一种优化,其中存在语言规则说将发生复制/移动的情况,但编译器可以选择不发生复制/移动。然而,在 * 所有这些情况下 *,即使复制/移动将被省略,它仍然必须 * 雅阁语言规则是可能的 *。
注意,C
17的“保证省略”是not really a form of "elision" per se;它重新定义了某些C++概念,使得没有复制/移动。但由于您的案例涉及一个命名对象,因此保证省略不适用。
所以如果你允许编译器省略一些东西也没关系;它仍然必须是可以发生在该对象上的事情。如果return pp2;意味着从pp2复制,则编译器上的省略设置无关紧要。无论pp2是什么,它必须是可复制的,否则会导致编译错误。
所以唯一的问题是:为什么return pp2;尝试从pp2复制,而不是从它 * 移动 ?通常,return identifier;将从identifier移动。但这是有规矩的。
特别是:
隐式可移动实体是自动存储持续时间的变量,它是非易失性对象或对非易失性对象类型的右值引用。
那么,结构化绑定的名称是什么呢?好吧,对于通过get<i>调用枚举元素的结构化绑定,它们是引用。但是,如果用于初始化引用的初始化器是prvalue,则它们仅是右值引用。
使用的初始化器是get<i>(e),其中e是所讨论的值。get<i>对于pair返回左值引用或右值引用。它 * 从不 * 返回纯右值(因为这将是对象的副本)。
因此,pair的结构化绑定的名称永远不会是右值引用,因此它们不能在return语句中隐式移动。return pp2;将尝试从中复制。
应该注意的是,结构化绑定的其他两种形式(数组和公共可访问的结构体成员)
也 * 永远不会允许结构化绑定的元素被隐式移动。对于结构体和数组元素,名称引用某个对象的子对象。它们不是引用,但也不是对象。因此,两者都不符合隐性变动的条件。
简而言之,您不应该期望将结构化绑定的元素隐式移动。

相关问题