{"id":105693,"date":"2021-09-16T07:00:00","date_gmt":"2021-09-16T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=105693"},"modified":"2021-10-08T10:33:05","modified_gmt":"2021-10-08T17:33:05","slug":"20210916-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20210916-00\/?p=105693","title":{"rendered":"The C++ implicit assignment operator is a non-ref-qualified member, even if the base class&#8217;s assignment has a ref-qualifier"},"content":{"rendered":"<p>Consider the following C++ class:<\/p>\n<pre>struct Base\r\n{\r\n    Base&amp; operator=(Base const&amp;) &amp; = default;\r\n\r\n    void BaseMethod();\r\n};\r\n<\/pre>\n<p>This defines a class for which you can assign to an lvalue reference, but not to an rvalue reference.<\/p>\n<pre>extern Base GetBase();\r\n\r\nBase b;\r\nb = GetBase(); \/\/ allowed\r\nGetBase() = b; \/\/ not allowed\r\n<\/pre>\n<p>Assigning to an rvalue is not generally useful, since the object has no name, and consequently it is difficult to do anything with the assigned-to object afterward.\u00b9<\/p>\n<p>Great, we got rid of assignment to a temporary, which we&#8217;ve seen <a href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20200729-00\/?p=104017\"> has been a source of confusion<\/a>.<\/p>\n<p>Now consider this:<\/p>\n<pre>struct Derived : Base\r\n{\r\n};\r\n\r\nDerived d;\r\nDerived() = d; \/\/ is this allowed?\r\n<\/pre>\n<p>We created a derived class and inherited the assignment operator from it. Do you expect the inherited assignment operator to block rvalues?<\/p>\n<p>You probably guessed that the answer is <i>no<\/i>, seeing as I gave it away in the title.<\/p>\n<p>The reason is that I lied when I said that the assignment operator was inherited. It was not inherited. It was <i>implicitly declared<\/i>.<\/p>\n<p>The rules for implicit declaration of the copy assignment operator are spelled out in <a href=\"https:\/\/timsong-cpp.github.io\/cppwp\/class.copy.assign\"> <b>[class.copy.assign]<\/b><\/a>, paragraphs 2 and 4. The short version is that a class is eligible for an implicitly-declared copy assignment operator if its base classes and non-static members all have a copy assignment operator. (Analogous rules apply for the implicitly-declared move assignment operator.)<\/p>\n<p>The catch is that the implicitly-declared copy assignment and move assignment operators are declared as an <i>unqualified<\/i> assignment operator, regardless of the reference-qualifications of the base classes and members. In our example, we get<\/p>\n<pre>struct Derived : Base\r\n{\r\n    \/\/ compiler autogenerates these\r\n    Derived&amp; operator=(Derived const&amp;) = default;\r\n    \/\/                                ^ no &amp;\r\n};\r\n<\/pre>\n<p>The lack of a ref-qualification means that this assignment operator applies equally to lvalues and rvalues.<\/p>\n<p>Our attempt to block rvalue assignment fails to propagate to derived classes!<\/p>\n<p>In order to repair this, each derived class must redeclare its assignment operator as lvalue-only.<\/p>\n<pre>struct Derived : Base\r\n{\r\n    <span style=\"color: blue;\">Derived&amp; operator=(Derived const&amp;) &amp; = default;<\/span>\r\n};\r\n<\/pre>\n<p>Oh, we&#8217;ve only started our journey down the rabbit-hole.<\/p>\n<p>At least for now, explicitly declaring a copy assignment operator does not cause the implicitly-declared copy\/move constructors to disappear, but the behavior is noted as deprecated in the C++ language specification, with the note that a future version of the language may indeed delete them.<\/p>\n<pre>Derived d;\r\nDerived d2{ d }; \/\/ on borrowed time\r\n<\/pre>\n<p>To make sure you don&#8217;t run into trouble in the future, you&#8217;ll want to declare them explicitly.<\/p>\n<pre>struct Derived : Base\r\n{\r\n    <span style=\"color: blue;\">Derived(Derived const&amp;) = default;<\/span>\r\n    <span style=\"color: blue;\">Derived(Derived&amp;&amp;) = default;<\/span>\r\n    Derived&amp; operator=(Derived const&amp;) &amp; = default;\r\n};\r\n<\/pre>\n<p>Great, we&#8217;ve restored the copy and move constructors.<\/p>\n<p>But explicitly declaring any constructors causes us to lose the implicitly-declared default constructor.<\/p>\n<pre>Derived d; \/\/ doesn't work any more\r\n<\/pre>\n<p>We&#8217;ll have to bring that back too.<\/p>\n<pre>struct Derived : Base\r\n{\r\n    <span style=\"color: blue;\">Derived() = default;<\/span>\r\n    Derived(Derived const&amp;) = default;\r\n    Derived(Derived&amp;&amp;) = default;\r\n    Derived&amp; operator=(Derived const&amp;) &amp; = default;\r\n};\r\n<\/pre>\n<p>The same exercise applies if we also want to block the move assignment operator to rvalues, but it&#8217;s more urgent because an explicit declaration of a move assignment operator does delete both the copy and move constructors even in C++20.<\/p>\n<pre>struct Base\r\n{\r\n    Base&amp; operator=(Base const&amp;) &amp; = default;\r\n    <span style=\"color: blue;\">Base&amp; operator=(Base&amp;&amp;) &amp; = default;<\/span>\r\n\r\n    void BaseMethod();\r\n};\r\n\r\nstruct Derived : Base\r\n{\r\n    Derived() = default;\r\n    Derived(Derived const&amp;) = default;\r\n    Derived(Derived&amp;&amp;) = default;\r\n    Derived&amp; operator=(Derived const&amp;) &amp; = default;\r\n    <span style=\"color: blue;\">Derived&amp; operator=(Derived&amp;&amp;) &amp; = default;<\/span>\r\n};\r\n<\/pre>\n<p>Phew, that was annoying.<\/p>\n<p>\u00b9 I mean, I guess you could do this:<\/p>\n<pre>Base b;\r\n\r\nSomething(GetBase() = b);\r\n(GetBase() = b).BaseMethod();\r\n<\/pre>\n<p>but it seems pointless to go to the effort of asking <code>Get\u00adBase<\/code> to create you a <code>Base<\/code> object, only to overwrite it with your own. You may as well just create your own temporary.<\/p>\n<pre>Something(Base(b));\r\nBase(b).BaseMethod();\r\n<\/pre>\n<p>Or, if you didn&#8217;t even mean to create a temporary, just use the original value:<\/p>\n<pre>Something(b);\r\nb.BaseMethod();\r\n<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Does that count as dis-qualification?<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[25],"class_list":["post-105693","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>Does that count as dis-qualification?<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/105693","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=105693"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/105693\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=105693"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=105693"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=105693"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}