【问题标题】:Move constructor and move assignment operator vs. copy elision移动构造函数和移动赋值运算符与复制省略
【发布时间】:2019-09-29 22:15:18
【问题描述】:

相关问题:

我发布这个问题是因为移动语义这个东西真的让我很困惑。起初它们对我来说似乎很清楚,但是当我试图向自己展示它们的用途时,我意识到也许我误解了一些东西。

我尝试将以下文件安排为使用移动语义的类矢量类的不那么简单的实现(实际上main 函数也在那里,以及一个免费函数使打印到屏幕更容易,...)。这不是一个真正的最小工作示例,但它产生到屏幕的输出是相当可读的,恕我直言。

不过,如果你觉得最好把它瘦下来,请建议我该怎么做。

反正代码如下,

#include<iostream>
using namespace std;

int counter = 0; // to keep count of the created objects

class X {
  private:
    int id = 0; // hopefully unique identifyier
    int n = 0;
    int * p;
  public:
    // special member functions (ctor, dtor, ...)
    X()           : id(counter++), n(0),   p(NULL)       { cout << "default ctor (id " << id << ")\n"; }
    X(int n)      : id(counter++), n(n),   p(new int[n]) { cout << "param ctor (id " << id << ")\n"; };
    X(const X& x) : id(counter++), n(x.n), p(new int[n]) {
      cout << "copy ctor (id " << id << ") (allocating and copying " << n << " ints)\n";
      for (int i = 0; i < n; ++i) {
        p[i] = x.p[i];
      }
    };
    X(X&& x)      : id(counter++), n(x.n), p(x.p) {
      cout << "move ctor (id " << id << ")\n";
      x.p = NULL;
      x.n = 0;
    };
    X& operator=(const X& x) {
      cout << "copy assignment (";
      if (n < x.size() && n > 0) {
        cout << "deleting, ";
        delete [] p;
        n = 0;
      }
      if (n == 0) {
        cout << "allocating, and ";
        p = new int[n];
      }
      n = x.size();
      cout << "copying " << n << " values)";
      for (int i = 0; i < n; ++i) {
        p[i] = x.p[i];
      }
      cout << endl;
      return *this;
    };
    X& operator=(X&& x) {
      this->n = x.n;
      this->p = x.p;
      x.p = NULL;
      x.n = 0;
      cout << "move assignment (\"moving\" " << this->n << " values)\n";
      return *this;
    };
    ~X() {
      cout << "dtor on id " << id << " (array of size " << n << ": " << *this << ")\n";
      delete [] p;
      n = 0;
    }
    // getters/setters
    int size() const { return n; }

    // operators
    int& operator[](int i) const { return p[i]; };
    X operator+(const X& x2) const {
      cout << "operator+\n";
      int n = min(x2.size(), this->size());
      X t(n);
      for (int i = 0; i < n; ++i) {
        t.p[i] = this->p[i] + x2.p[i];
      }
      return t;
    };

    // friend function to slim down the cout lines
    friend ostream& operator<<(ostream&, const X&);
};

int main() {
    X x0;
  X x1(5);
  X x2(5);
  x1[2] = 3;
  x2[3] = 4;
  cout << "\nx0 = x1 + x2;\n";
  x0 = x1 + x2;
  cout << "\nX x4(x1 + x2);\n";
  X x4(x1 + x2);
  cout << x4 << endl;
  cout << '\n';
}

// function to slim down the cout lines
ostream& operator<<(ostream& os, const X& x) {
  os << '[';
  for (int i = 0; i < x.size() - 1; ++i) {
    os << x.p[i] << ',';
  }
  if (x.size() > 0) {
    os << x.p[x.size() - 1];
  }
  return os << ']';
}

当我编译并运行它时

$ clear && g++ moves.cpp && ./a.out

输出如下(#-cmets 是手动添加的)

default ctor (id 0)
param ctor (id 1)
param ctor (id 2)

x0 = x1 + x2;
operator+
param ctor (id 3)
move assignment ("moving" 5 values)
dtor on id 3 (array of size 0: [])

X x4(x1 + x2);
operator+
param ctor (id 4)
[0,0,3,4,0]

dtor on id 4 (array of size 5: [0,0,3,4,0])
dtor on id 2 (array of size 5: [0,0,0,4,0])
dtor on id 1 (array of size 5: [0,0,3,0,0])
dtor on id 0 (array of size 5: [0,0,3,4,0])

从输出的第一部分,我想我确实演示了移动赋值运算符的预期用途。在这方面我是对的吗? (从下一个输出看,我好像不是,但我不确定。)

此时,如果我关于复制省略阻止调用复制 ctor 的推断是正确的,那么我自然会想到一个问题 (and not only me, see OP's comment here):

基于另一个临时对象(egx4 基于X x4(x1 + x2);x1 + x2 的结果)创建对象的情况不正是这种情况吗?将语义移到应该引入的地方?如果不是,说明移动 ctor 使用的基本示例是什么?

然后我读到可以通过添加适当的选项来防止复制省略。

输出

clear && g++ -fno-elide-constructors moves.cpp && ./a.out 

但是,如下:

default ctor (id 0)
param ctor (id 1)
param ctor (id 2)

x0 = x1 + x2;
operator+
param ctor (id 3)
move ctor (id 4)
dtor on id 3 (array of size 0: [])
move assignment ("moving" 5 values)
dtor on id 4 (array of size 0: [])

X x4(x1 + x2);
operator+
param ctor (id 5)
move ctor (id 6)
dtor on id 5 (array of size 0: [])
move ctor (id 7)
dtor on id 6 (array of size 0: [])
[0,0,3,4,0]

dtor on id 7 (array of size 5: [0,0,3,4,0])
dtor on id 2 (array of size 5: [0,0,0,4,0])
dtor on id 1 (array of size 5: [0,0,3,0,0])
dtor on id 0 (array of size 5: [0,0,3,4,0])
+enrico:CSGuild$ 

看起来我期望的对 move ctor 的调用现在在那里,但是该调用和对 move assignment 的调用都在每个调用之前对 move ctor 进行了另一个调用。

为什么会这样?我完全误解了移动语义的含义吗?

【问题讨论】:

  • 就“瘦身”而言...您这里有用于复制和移动分配的代码,这与您的实际问题完全无关(围绕移动/复制构造)仅受X x4(x1 + x2)这一行的刺激
  • X x4; /* ... */; x4 = x1 + x2; 将是移动语义用例的更好示例
  • @donkopotamus,我希望我对问题的编辑澄清我对移动赋值运算符也有疑问。无论如何,我可以删除主要的一些行。我今晚会做。

标签: c++ c++11 move-semantics move-constructor copy-elision


【解决方案1】:

您在这里似乎有两个问题:

  • 为什么不为X x4(x1 + x2) 调用移动构造函数?
  • 为什么在禁用复制省略时,移动构造函数会被调用两次?

第一个问题

这不正是那个情况 (X x4(x1 + x2);) 应该在哪里引入语义?

嗯,不。为了使用移动语义,您实际上是在建议我们应该选择operator+ 中构造一个X,然后将其移动 到结果x4 ,与在operator+ 期间就位的复制省略构造最终结果(x4)相比,这显然是低效的

第二个问题

禁用复制省略后,为什么我们会在X x4(x1 + x2) 期间看到两次对移动构造函数的调用?考虑一下这里有三个作用域:

  1. operator+ 范围,我们在其中构造一个 X 并返回它;
  2. 我们调用X x4(x1 + x2)main范围;
  3. X constructor,我们从x1 + x2 构造X

那么,在没有省略的情况下,编译器是:

  • 将结果从operator+移动到main(到x1 + x2);和
  • x1 + x2的内容移动到x4中。

【讨论】:

  • 两个移动是从toperator+的局部变量)移动到结果对象(本例中是临时的),以及将结果对象移动到x4
  • @M.M 这就是我在最后两个要点中想说的(显然非常不清楚!)
  • @donkopotamus,如何编辑代码以演示移动赋值运算符和移动 ctor 的使用?顺便说一句,最后两个要点对我来说很清楚。
  • @donkopotamus,我认为显示对移动 ctor 的调用可能是一条很好的线路,例如 X x(X(3));,我错了吗?
猜你喜欢
  • 2016-05-19
  • 2023-03-03
  • 2017-05-15
  • 2020-09-06
  • 1970-01-01
  • 2020-03-22
  • 2013-03-16
  • 2014-01-02
  • 2017-01-16
相关资源
最近更新 更多