定义了移动构造函数,会导致赋值操作符被删除? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
amiwrong123
V2EX    C++

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

  •  
  •   amiwrong123 2022-01-18 00:54:53 +08:00 2898 次点击
    这是一个创建于 1429 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    既然需要深度拷贝,那么默认的拷贝构造就肯定不能满足要求,因为它仅执行简单的浅拷贝。这样一来使用时很有可能造成 double free 、野指针这种问题,所以编译器索性不生成默认的拷贝构造函数,直接给你一个编译错误,让你自己写拷贝构造函数。
    elfive
        6
    elfive  
       2022-01-18 07:53:53 +08:00 via iPhone   1
    @elfive #5 你的代码里,如果在移动构造函数 content 那里不使用 std::move 将 d.content 转换为右值引用,那么它实际上调用的是 std::string 的赋值构造函数,即代码里的这个移动构造函数和拷贝构造函数没区别
    jackchenly
        7
    jackchenly  
       2022-01-18 08:51:43 +08:00 via iPad
    应该是这样的,我也发现了,至于为什么,请看上面几位老哥
    amiwrong123
        9
    amiwrong123  
    OP
       2022-01-19 00:24:14 +08:00
    @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
       2022-01-19 01:04:01 +08:00
    @dangyuluo #2
    @elfive #5
    ![]( https://i.bmp.ovh/imgs/2022/01/c7d19607254b1f4d.png)
    还想问个问题,红框里这种句是什么情况阿?前面那句倒是理解了,就是 rule of five 。
    amiwrong123
        11
    amiwrong123  
    OP
       2022-01-19 01:09:22 +08:00
    @elfive #6
    就是说,这样,string 才会调用 Move constructor 。
    但我看了写资料,说 std::move 只是相当于一个 static_cast<T&&>而已,并没有做任何移动操作。而移动操作,实际上是 一个接管的动作。

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

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

    至于为什么有时候编译器不能自己生成默认的移动构造函数,即使没有定义拷贝构造函数,那是因为当类的非 static 成员中含有必须在构造时初始化的成员,例如:引用、const 类型定义。
    通俗一点来说,就是必须在构造函数初始化列表中初始化的变量。因为这些成员不可移动。他们的存在,就会让编译器决定不生成默认的移动构造函数。
    amiwrong123
        14
    amiwrong123  
    OP
       2022-01-20 00:03:45 +08:00
    @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
       2022-01-20 00:39:43 +08:00
    @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/1/8f86784bc173f8b1.png)
    我理解了,因为移动构造可能抛出异常,所以 vector 扩容时,不会使用这种移动构造函数。但为什么“所以只能调用 copy constructor”,难道拷贝构造函数 就不可能抛出异常了吗?

    (总是就是本着不懂就问的原则,但我也尽量去查找资料了)
    amiwrong123
        16
    amiwrong123  
    OP
       2022-01-20 01:11:46 +08:00
    @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     Solana     4455 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 30ms UTC 09:59 PVG 17:59 LAX 01:59 JFK 04:59
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86