V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
amiwrong123
V2EX  ›  C++

定义了移动构造函数,会导致赋值操作符被删除?

  •  
  •   amiwrong123 · 121 天前 · 1341 次点击
    这是一个创建于 121 天前的主题,其中的信息可能已经有所发展或是发生改变。

    https://www.cnblogs.com/pointer-smq/p/5297682.html 这篇文章说 这条语句 label = 2 会让编译器隐式调用 Token 的构造函数用 2 构造一个 Token ,参数的_content 采用默认值“”,然后又调用隐式生成的移动赋值(move assignment)函数,进行赋值。

    我就根据程序,再加一个移动构造函数,却发现报错了。

    #include <iostream>
    using namespace std;
    struct Token
    {
        int label;
        string content;
        Token(int _label = -1, string _content = "")
            : label(_label)
            , content(_content)
        {
            cout << "defalut" << endl;
        }
    
        Token(Token&& d)
            : label(d.label)
            , content(d.content)
        {
            cout << "move" << endl;
        }
    };
    
    int main()
    {
        Token label(1, "hello");
    
        label = 2; //这 tm 是合法的!!!
    	return 0;
    }
    

    报错:无法引用 函数 "Token::operator=(const Token &)" (已隐式声明) -- 它是已删除的函数。这是为啥啊?

    还有,文中说:当你写一个赋值语句的时候,编译器会首先检查两个类型又没有直接实现的赋值函数,然后检查赋值左右的类型是否能做隐式转换和构造,转换或者构造好之后,再尝试进行拷贝或移动赋值。这是顺序是对的吗?

    比如 label = 2 这一步,

    • 如果 operator=有定义,就直接执行 operator=这一步就行呗(即只有一个步骤)?
    • 如果 operator=没有定义,那就得先执行 拷贝函数函数弄个临时变量,再调用移动构造函数(肯定有两个步骤)?

    (比较菜,大佬们轻喷)

    16 条回复    2022-01-20 01:11:46 +08:00
    qaweqa
        1
    qaweqa  
       120 天前
    可能有了移动构造 就不自动生成拷贝构造了吧
    dangyuluo
        2
    dangyuluo  
       120 天前 via iPhone   ❤️ 3
    rule of five
    当你定义其中任一个的时候,说明你的类可能在管理某类资源,这时候默认生成的拷贝 /移动构造器,析构函数,拷贝 /复制赋值函数应该是不正确的,编译器索性不生成了
    yulon
        4
    yulon  
       120 天前
    兄弟说句实话,你愿意找文章看肯定是好的,但是 cppreference 上面就能找到的东西,天天这么问,一百年都学不完 C++ 的,标准就是这么定的,哪有那么多为什么啊,这还只是标准里的内容,离实践还远着呢。
    elfive
        5
    elfive  
       120 天前 via iPhone   ❤️ 1
    首先,要知道只有需要深度拷贝的类或结构体定义移动构造才有实际意义。

    既然需要深度拷贝,那么默认的拷贝构造就肯定不能满足要求,因为它仅执行简单的浅拷贝。这样一来使用时很有可能造成 double free 、野指针这种问题,所以编译器索性不生成默认的拷贝构造函数,直接给你一个编译错误,让你自己写拷贝构造函数。
    elfive
        6
    elfive  
       120 天前 via iPhone   ❤️ 1
    @elfive #5 你的代码里,如果在移动构造函数 content 那里不使用 std::move 将 d.content 转换为右值引用,那么它实际上调用的是 std::string 的赋值构造函数,即代码里的这个移动构造函数和拷贝构造函数没区别
    jackchenly
        7
    jackchenly  
       120 天前 via iPad
    应该是这样的,我也发现了,至于为什么,请看上面几位老哥
    amiwrong123
        9
    amiwrong123  
    OP
       120 天前
    @dangyuluo #2
    所以,Copy constructor 、Move constructor 、Copy assignment operator 、Move assignment operator 、Destructor 这五个东西,只要用户自己定义了其中一个,那么其他的 都会被删除。
    ```cpp
    #include <iostream>
    using namespace std;
    struct Token
    {
    int label;
    string content;
    Token(int _label = -1, string _content = "")
    : label(_label)
    , content(_content)
    {
    cout << "defalut" << endl;
    }

    Token(Token&& d)
    : label(d.label)
    , content(d.content)
    {
    cout << "move" << endl;
    }

    Token& operator=(Token&&) = default;//不加这句,就会报错
    };

    int main()
    {
    Token label_1(1, "hello");

    label_1 = Token(2, "hell");//这里是一个临时变量,所以属于一个右值。所以必须用 Move assignment operator
    return 0;
    }
    ```

    所以这个程序,就验证了呗( c++基础不是很扎实,所以想确认一下子)😂
    amiwrong123
        10
    amiwrong123  
    OP
       119 天前
    @dangyuluo #2
    @elfive #5
    ![]( https://i.bmp.ovh/imgs/2022/01/c7d19607254b1f4d.png)
    还想问个问题,红框里这种句是什么情况阿?前面那句倒是理解了,就是 rule of five 。
    amiwrong123
        11
    amiwrong123  
    OP
       119 天前
    @elfive #6
    就是说,这样,string 才会调用 Move constructor 。
    但我看了写资料,说 std::move 只是相当于一个 static_cast<T&&>而已,并没有做任何移动操作。而移动操作,实际上是 一个接管的动作。

    我现在就很难以理解 移动操作。也很好奇 string 的移动操作是怎么做的。

    我就说下我简单的理解,就好比:
    - 之前,旧对象要被 delete ,新对象要被 new 出来
    - 现在,新对象不 new 了,直接指向了旧对象,旧对象不用被 delete 了
    dangyuluo
        12
    dangyuluo  
       119 天前   ❤️ 1
    @amiwrong123 给你推荐个网站,Cppinsights.io ,可以查看你的类在编译器眼里的样子。
    elfive
        13
    elfive  
       119 天前 via iPhone   ❤️ 1
    @amiwrong123 #11 std::move 他本来也不需要做任何事情,显式使用 std::move 是让编译器知道你要调用移动构造或者移动拷贝函数(左值引用不能自动转为右值引用,但反过来可以),还有一个目的是让程序员知道,被移动的变量在这条语句之后不能再次访问以获取任何有效的内容,因为 move 之后,变量内容就被“重置”了

    至于为什么有时候编译器不能自己生成默认的移动构造函数,即使没有定义拷贝构造函数,那是因为当类的非 static 成员中含有必须在构造时初始化的成员,例如:引用、const 类型定义。
    通俗一点来说,就是必须在构造函数初始化列表中初始化的变量。因为这些成员不可移动。他们的存在,就会让编译器决定不生成默认的移动构造函数。
    amiwrong123
        14
    amiwrong123  
    OP
       119 天前
    @dangyuluo #12
    谢谢,这个网站很好用。

    ```cpp
    #include <iostream>
    using namespace std;

    class A {
    public:
    A() {
    cout <<"default constructor" << endl;
    }

    A(const A& x) {
    cout <<"copy constructor" << endl;
    }

    A(A&& x) {
    cout <<"move constructor" << endl;
    }

    A& operator = (const A& x) {
    cout <<"copy operator =" << endl;
    return *this;
    }

    A& operator = (A&& x) {
    cout <<"move operator =" << endl;
    return *this;
    }

    };

    int main() {
    A a; // default constructor
    A b(a); // copy constructor
    A c = a; // copy constructor
    c = b; // copy operator =
    c = A(); // move operator =
    A d = A(); // move constructor
    return 0;
    }
    ```
    然后我又试了一下这个程序,这句 A d = A();我真的有点没懂,我以为它会使用默认构造函数创建一个临时对象出来,然后由于这个临时对象是右值,所以我觉得它会调用 move constructor 来构造 d ,但是却戛然而止了。



    @dangyuluo #12
    @elfive #13
    两位老哥帮忙看一下把

    ![]( https://i.bmp.ovh/imgs/2022/01/f7d8184635cbee59.png)
    好吧,刚想完,结果自己找到了答案。


    但又有了新的问题。
    这个 纯右值临时量 (C++11 起)(C++17 前),为什么这么写,所以它 只存在于 11 到 17 之间吗(哎,咋这么复杂)
    amiwrong123
        15
    amiwrong123  
    OP
       119 天前
    @elfive #13
    看了很多文章,大概懂了。我说下理解

    1. std::move 只是强制类型转换
    2. 使用 std::move ,是为了编译器能够使用到 Move constructor 或 Move assignment operator 。
    3. 移动操作本质只是,将一个指针复制给另一个指针,再将初始指针置为 null ( C++ 的移动 move 是怎么运作的? - Tanki Zhang 的回答 - 知乎
    https://www.zhihu.com/question/277908001/answer/396469410 这个回答证明了我的猜想)
    4. 移动操作只能针对堆上的对象,因为这样才有意义。(你一个栈上的对象,反正都要被析构,重用不重用这块内存又有何妨)(不知道这么说,是不是太绝对了,如果有误,望老哥指正)

    ![]( https://i.bmp.ovh/imgs/2022/01/8f86784bc173f8b1.png)
    我理解了,因为移动构造可能抛出异常,所以 vector 扩容时,不会使用这种移动构造函数。但为什么“所以只能调用 copy constructor”,难道拷贝构造函数 就不可能抛出异常了吗?

    (总是就是本着不懂就问的原则,但我也尽量去查找资料了😂)
    amiwrong123
        16
    amiwrong123  
    OP
       118 天前
    @elfive #13
    ```cpp
    #include <iostream>
    using namespace std;

    class A {
    public:
    A() {
    cout << "default constructor" << endl;
    }

    A(const A& x) {
    cout << "copy constructor" << endl;
    }

    A(A&& x) {
    cout << "move constructor" << endl;
    }

    A& operator = (const A& x) {
    cout << "copy operator =" << endl;
    return *this;
    }

    A& operator = (A&& x) {
    cout << "move operator =" << endl;
    return *this;
    }

    };

    A returnValue() {
    return A();
    }

    A&& returnValue_2() {
    return A();
    }

    int main() {
    A e = returnValue(); // move constructor
    A d = returnValue_2(); // move constructor
    return 0;
    }
    ```
    打印结果为:
    default constructor
    default constructor
    move constructor

    现在 A e = returnValue(); 只打印一句。我认为是函数中构造对象,打印了这句。然后进行了 RVO 优化,就少打印了一次。然后进行了复制初始化时的优化(我 14 楼 贴的图),又少打印了一次。所以最后只有一次。

    然后 A d = returnValue_2();打印了两句。我就有点不明白,为什么返回值是 A&&,就能强制调用 move constructor 了? returnValue()理论上也是一个右值阿。

    (抱歉问题很多)
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1831 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 20ms · UTC 16:59 · PVG 00:59 · LAX 09:59 · JFK 12:59
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.