【问题标题】:problems with Move constructor and Move overloaded assignment operator?移动构造函数和移动重载赋值运算符的问题?
【发布时间】:2021-08-18 16:10:46
【问题描述】:

fredoverflow(user 237K Rep.) 在他的两个答案中解释了大多数事情

但是在实现 Move 构造函数和重载的 Move 赋值运算符 (OMAO)(我在整个问题中使用这些简短形式)时,我遇到了一些问题,我将在这里提出。

用户 Greg Hewgill (拥有 826K 代表的用户)还有另一个答案。 https://stackoverflow.com/a/3106136/11862989his
我在引用他,

假设你 有一个函数返回一个实质对象然后是一个普通的 C++ 编译器将为 multiply() 的结果创建一个临时对象, 调用复制构造函数初始化 r,然后销毁 临时返回值。 C++0x 中的移动语义允许“移动 构造函数”被调用以通过复制其内容来初始化 r,并且 然后丢弃临时值而不必破坏它。

我也会提到这个问题。

好的,现在开始

代码

.cpp

#include"34_3.h"
#include<iostream>
#include<conio.h>
#include<cstring>

A::A()                                       // O arg ctor
{
    std::cout<<"0 arg constructor\n";
    p=0;
    s=nullptr;
        
}

A::A(int k1,const char *str)                 // 2 arg ctor
{
    std::cout<<"2 arg constructor\n";
    
    p=k1;
    s=new char[strlen(str)+1];
    strcpy(s,str);
    
}

A::A(const A &a)                             // copy ctor
{
    std::cout<<"copy constructor\n";
    
    p=a.p;
    s=new char[strlen(a.s)+1];
    strcpy(s,a.s);
    
}

A::A(A &&a)                                   // Move ctor
{
    std::cout<<"Move constructor\n";
    
    p=a.p;
    s=new char[strlen(a.s)+1];
    strcpy(s,a.s);
    a.s=nullptr;
    
}


A& A::operator=(const A &a)                 // Overloaded assignement opeator `OAO`
{
    std::cout<<"overloade= operator\n";
    
    p=a.p;
    s=new char[strlen(a.s)+1];
    strcpy(s,a.s);
    return *this;
    
}


A& A::operator=(A &&a)                        // `OMAO`
{
    std::cout<<"Move overloade = operator\n";
    
    p=a.p;
    s=new char[strlen(a.s)+1];
    strcpy(s,a.s);
    a.s=nullptr;
    return *this;
    
}

A::~A()                                       // Dctor
{
    delete []s;
    std::cout<<"Destructor\n";
    
}

void A::display()
{
    std::cout<<p<<" "<<s<<"\n";
    
}

.h

#ifndef header
#define header

struct A
{
    private:
        int p;
        char *s;
    public:
        A();                            //  0 arg ctor
        A(int,const char*);             //  2 arg ctor
        A(const A&);                    //  copy ctor
        A(A&&);                         //  Move ctor
        
        A& operator=(const A&);         // `OAO`
        A& operator=(A&&);              // `OMAO`
        
        ~A();                           // dctor
        
        void display(void);
        
};
#endif

我在这里放了几个主要函数及其输出,以便我可以轻松地讨论这个问题。

1_main

A make_A();
int main()
{
    A a1=make_A();
    
    a1.display();
    
}
A make_A()
{
    A a(2,"bonapart");
    return a;
    
}

输出

2 arg constructor
2 bonapart
Destructor
  1. 为什么它不执行 Move 构造函数,但是如果我注释掉 .cpp 文件中的 Move 构造函数定义和 .h 文件中的声明,那么它会给出错误 [Error] no matching function for call to 'A::A(A)',如果我使用这个 A a1=std::move(make_A()); 然后 Move 构造函数调用,那么为什么这是怎么回事?
  2. 为什么 make_A() 函数中对象 a 的析构函数没有运行?

2_main()

A make_A();
int main()
{
    A a1;
    a1=make_A();
    
    a1.display();
    
}
A make_A()
{
    A a(2,"bonapart");
    return a;
    
}

输出

0 arg ctor
2 arg ctor
Move overloade = operator
copy ctor
Dctor
Dctor
2 bonapart
Dctor
  1. 现在复制构造函数和析构函数运行在由于从 Move 重载 = 运算符函数返回 *this 而创建的临时对象。根据 Greg Hewgill 声明 C++ 0x 允许调用 Move 构造函数以通过复制其内容进行初始化,然后丢弃临时值而不必破坏它。我正在使用C++11,但仍然通过创建临时对象、复制构造函数来完成初始化。
  2. 我没有得到第二个析构函数正在为哪个对象运行?

3_main

fredoverflow(用户 237K Rep.)保留 Move 重载运算符 A&amp; 的返回类型,但我认为这是错误的。

A make_A();
int main()
{
    A a1,a2;
    a2=a1=make_A();
    
    a1.display();
    a2.display();
    
}
A make_A()
{
    A a(2,"bonapart");
    return a;
    
}

输出

[Error] prototype for 'A& A::operator=(A&&)' does not match any in class 'A'

所以我觉得返回类型应该是A&amp;&amp;AA&amp;&amp; 也给出错误[ERROR] can't bind a lvalue to a&amp;&amp;

所以返回类型必须是A,对吗?

4

在 Move 构造函数和 Move 重载 = 运算符中,我使用了 a.s=nullptr; 这个语句总是在 Move 语义中使用 fredoverflow(user) 解释了类似“现在源不再拥有它的对象”但我没有得到它。因为如果我不写这个声明仍然没有问题,一切正常。请解释这一点

【问题讨论】:

  • 在您的“移动”构造函数和赋值运算符中,您有声明a.s=nullptr;。这将导致泄漏,因为分配给a 的内存不再被释放。您应该“移动”指针,而不是重新分配和复制(这就是复制构造函数和赋值运算符所做的)。例如只需交换指针:std::swap(s, a.s);(但请记住在构造函数中将s 初始化为nullptr first)。
  • 您想要答案还是答案? ;) 请不要到处使用缩写
  • 1_main 移动构造函数由于复制省略未执行。尝试A a1(std::move(make_A()));A a1=std::move(make_A()); 使其显式调用
  • 你应该只问你的问题而不用所有的前言和胡扯其他问题和答案
  • 每个问题一个问题。此外,第一个“子问题”与What are copy elision and return value optimization? 重复

标签: c++ move destructor move-semantics rvalue-reference


【解决方案1】:

您的班级A 有几个问题:

  • 您的赋值运算符不处理自赋值和泄漏:

    A& A::operator=(const A& a)
    {
        std::cout<<"overload operator=\n";
        if (this != &a)
        {
            p = a.p;
            delete[] s;
            s = new char[strlen(a.s) + 1];
            strcpy(s, a.s);
        }
        return *this;
    }
    
  • 你的移动不是移动而是复制:

A::A(A&& a) : p(a.p), s(a.s)
{
    a.s = nullptr;
    std::cout << "Move constructor\n";
}

A& A::operator=(A&& a)
{
    std::cout << "Move overload operator=\n";

    if (this != &a) {
        p = a.p;
        delete [] s;
        s = a.s;
        a.s = nullptr;
    }
    return *this;
}

现在,关于

A make_A()
{
    A a(2,"bonapart"); // Constructor
    return a;
}

由于潜在的复制省略 (NRVO),有几种情况 (gcc 的标志为-fno-elide-constructors 来控制它)

如果 NRVO 适用,则 a 是“就地”构造的,因此不会发生额外的破坏/移动;

还有一个move构造函数和a的销毁。

A make_A()
{
    A a(2,"bonapart"); // #2 ctor(int const char*)
    return a; // #3 move (elided with NRVO)
} // #4 destruction of a, (elided with NRVO)

int main()
{
    A a1; // #1: default ctor
    a1 = // #5: Move assignment (done after make_A)
      make_A(); // #6: destructor of temporary create by make_A

    
    a1.display();
} // #8: destructor of a1

使用 NRVO

default ctor
ctor(int const char*)
move assignment
destructor
display
destructor

没有 NRVO (-fno-elide-constructors)

default ctor
ctor(int const char*)
move ctor
destructor
move assignment
destructor
display
destructor

Demo

对于

A a1,a2;
a2 = a1 = make_A();

a1 = make_A(); 使用移动赋值。 a2 = (a1 = make_A()) 使用复制赋值作为移动赋值返回(正确)A&amp;

4 在 Move 构造函数和 Move 重载 = 运算符中,我使用了 a.s=nullptr; 这个语句总是在 Move 语义中使用 fredoverflow(user) 解释了类似“现在源不再拥有它的对象”但我没有得到它。因为如果我不写这个声明仍然没有问题,一切正常。请解释这一点

你的问题是你复制而不是移动。

如果你做s = a.s;而不是复制

s = new char[strlen(a.s) + 1];
strcpy(s, a.s);

那么this-&gt;sa.s 都会指向相同的数据,thisa 都会在它们的析构函数中释放(相同的)内存 -> 双重释放错误。

a.s = nullptr; 会解决这个问题。

【讨论】:

  • 谢谢,我理解您的回答,需要一些时间才能理解我必须学习您在回答中提到的一些新观点。我提到这个评论是为了让你知道我正在检查答案但没有忽略。好的。
  • 请注意:我相信任何需要自赋值检查才能“正确”工作的赋值运算符仍然存在问题!上面的例子也不例外:如果delete[] s; 之后的分配因异常而失败,则该对象将处于不可恢复的状态。我通常建议利用复制构造函数swap() 和析构函数:A(a).swap(*this); 如果您按值传递参数,则更容易:A&amp; operator=(A a)( a.swap(*this); return *this; }。在这种情况下,swap 很简单 (void swap(A&amp; a){ std::swap(a.s, this-&gt;s); }),但我仍然会保持这种模式。
  • @Jarod42 1. 使用 NRVO,您显示输出中的第 4 条语句是该析构函数正在运行的析构函数?它是否为amake_A() 函数返回的右值运行。您还提到了life time extension 之类的东西,您能对此有所了解吗?
  • 我用#n 编号以显示显示发生的位置。 #4 是局部变量a的销毁。生命周期延长是当你有const T&amp; ref = temporary; 时,然后当ref 超出范围而不是在语句末尾时,临时被销毁。首选A&amp; operator=。 Q1 关于“缺少”构造函数/析构函数是因为 NRVO。 (顺便说一句,更喜欢逐个问题地关注一个问题)。您可以在 Demo 中查看版本。请注意,C++17 有一些必需的省略(对于 RVO),在以前的版本中是可选的。
  • 延长寿命(函数参数不会发生)。
【解决方案2】:

1_main: 由于复制省略,移动构造函数没有被执行 交换完成后观察到额外的析构函数,临时对象被销毁。

2_main:与调用移动运算符的 1_main 中的观察相同

3_main:出现错误是因为您使用的是低版本的编译器。可能需要指定 -std=c++11

4:a.s=nullptr 不是移动的情况,因为您正在分配新的记忆并进行某种复制。例如

A::A(A &&a)                                   // Move ctor
{
    std::cout<<"Move constructor\n";       
    p=a.p;
    s=a.s;
    a.s=nullptr; 
    a.p=0;
}

【讨论】:

  • 构造函数没有返回类型
  • 注释掉的行应该在 in ,否则旧对象的破坏会在新对象中留下悬空指针
  • @M.M 我明白了,但想留下一个未定义的行为,所以将它们注释掉,将撤消它们
猜你喜欢
  • 2016-05-19
  • 2017-05-15
  • 2020-09-06
  • 1970-01-01
  • 2015-07-10
  • 2019-09-29
  • 1970-01-01
  • 2020-12-03
  • 2013-06-11
相关资源
最近更新 更多