[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Re: [lmi] Detecting whether move semantics actually take place
From: |
Greg Chicares |
Subject: |
Re: [lmi] Detecting whether move semantics actually take place |
Date: |
Sun, 31 Jul 2022 21:16:15 +0000 |
User-agent: |
Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Thunderbird/91.8.0 |
On 7/31/22 12:41, Vadim Zeitlin wrote:
> Sorry, I'd like to slightly amend what I wrote last night:
>
> On Sun, 31 Jul 2022 03:14:09 +0200 I wrote:
>
> Me> On Sun, 31 Jul 2022 00:41:37 +0000 Greg Chicares
> <gchicares@sbcglobal.net> wrote:
> Me>
> Me> GC> In the modified example below, struct 'non_movable' is constructible
> and
> Me> GC> assignable from 'non_movable&&' by using copy semantics, but not,
> AIUI,
> Me> GC> by using move semantics.
> Me>
> Me> It does use move semantics.
>
> It still does...
Thanks, this demonstrated a flaw in my conceptual model. I had thought
that, when a most-derived class (like the examples in this thread)
has implicitly-declared move and copy operations, and has a base class
lacking move operations, then we'd just get a copy instead of a move.
But there are different degrees of "lacking" move operations:
(A) Explicitly declared as deleted: participates in overload resolution
- "deleted" is unfortunate terminology: it's not as though it
potentially existed but wasn't allowed to be born--no, it's
present, as a compile-time error trap
- if it's chosen by overload resolution, the program is ill-formed
(B) Not declared: doesn't participate in overload resolution
- as though it had never been conceived, much less born
- it's a funky rule; I guess they needed it for c++98 compatibility
I had thought that either of these would act as an impediment to move
semantics, causing the most-derived class's (explicitly defaulted)
move operations to be defined as deleted. But they don't both impede
that; only the first one (= delete) does.
Thus, my top-level (mis)understanding was that the most-derived class's
move operations look for impediments
- const or reference members (spoil both move and copy assignment)
- non-movable data members
- non-movable base classes anywhere in the inheritance chain
and, if there's any impediment, moving is defined as deleted and only
copying is allowed. But I had imagined that "non-movable" was a unitary
concept--either you have move members, or you don't--whereas now it
seems to me that it's a tri-state property:
- move forbidden (= delete);
- move declared, whether explicitly or implicitly, and not
defined as deleted;
- move "not declared", as in the green boxes in the chart at the
bottom of this page:
https://howardhinnant.github.io/classdecl.html
so I'm going to have to watch his video
https://www.youtube.com/watch?v=vLinb2fgkHk
again and try to grok this "defaulted | deleted | inhibited"
trichotomy. [Edit: It becomes clearer to me below.]
I'm guessing the answer may be that deletion is infectious, but inhibition
is not. Thus, using the example you gave, when the most-derived class is
initialized from an rvalue reference, the reasoning is like this:
- int data member: just copy it because that's cheapest?
- move_detector member: it's explicitly moveable, so move it
- base class 'base': move operations are "not declared", which I guess
is synonymous with "inhibited", so that'll be [hypothesis] copied?
Investigation calls for some tooling, which you provide...
> Me> I can prove this experimentally:
>
> .. but I think the demonstration can be improved. Instead of manually
> defining copy and move ctors, which could be different from what the
> compiler does by default, let's rely on the default-generated ctors:
Add a mixin class to print which {move|copy} function is called: thanks.
Here, I believe you meant the copy ctor to print "copied":
> struct move_detector {
> explicit move_detector(int n) : n_{n} {}
> move_detector(move_detector const& x) : n_{x.n_} { printf("[%d] moved\n",
> n_); }
^^^^^
copied
> move_detector(move_detector&& x) : n_{x.n_} { printf("[%d] moved\n", n_);
> }
>
> int n_;
> };
[with that change, it still prints "moved" in your example]
Let's use that to test the hypothesis above. I'll modify your example,
transplanting the move_detector member to the base class, and changing the
prime number so there's no question whether I'm running the new 'a.out':
---------------------------------- >8 --------------------------------------
#include <stdio.h>
#include <utility>
struct move_detector {
explicit move_detector(int n) : n_{n} {}
move_detector(move_detector const& x) : n_{x.n_}
{ printf("[%d] copied\n", n_); }
move_detector(move_detector&& x) : n_{x.n_}
{ printf("[%d] moved\n", n_); }
int n_;
};
struct base {
~base() = default;
move_detector value{19};
};
struct non_movable : base {
non_movable() = default;
non_movable(non_movable const&) = default;
non_movable(non_movable&&) = default;
};
int main() {
non_movable x;
return non_movable{std::move(x)}.value.n_;
}
---------------------------------- >8 --------------------------------------
I predict that move_detector will be copied, so 'a.out' will print
"[19] copied" and return 19. Let's see:
/opt/lmi/src/lmi[0]$clang -Wall -std=c++20 eraseme.cpp && ./a.out || echo $?
[19] copied
19
Hypothesis confirmed, or, at least, not disproven.
This suggests to me that the {copy|move} duality is more complex than I had
imagined. I had thought that C++ strongly preferred copying (even if only
for C++98 compatibility), and would use move semantics only if every
subobject is moveable, but revert to copy semantics at the slightest whiff
of a lack of moveability--and in particular I thought that the most-derived
class would perceive non-moveability in a base class and itself revert to
copying. (Maybe I got that impression by reading countless online questions
like "Why did initialization from X&& work when there's no move ctor?", to
which the answer is usually "It matched X(X const&), so it got copied".)
But in this case, the most-derived class is actually moved, and its
non-moveable base is copied.
Can it flip back and forth both ways? Let's try:
---------------------------------- >8 --------------------------------------
#include <stdio.h>
#include <utility>
struct move_detector {
explicit move_detector(int n) : n_{n} {}
move_detector(move_detector const& x) : n_{x.n_}
{ printf("[%d] copied\n", n_); }
move_detector(move_detector&& x) : n_{x.n_}
{ printf("[%d] moved\n", n_); }
int n_;
};
struct base0 {
base0() = default;
base0(base0 const&) = delete;
base0(base0&&) = default;
move_detector value{13};
};
struct base1 : base0 {
~base1() = default;
};
struct non_movable : base1 {
non_movable() = default;
non_movable(non_movable const&) = default;
non_movable(non_movable&&) = default;
};
int main() {
non_movable x;
return non_movable{std::move(x)}.value.n_;
}
---------------------------------- >8 --------------------------------------
Wow.
Skip gcc and just use clang here. This is why the
https://howardhinnant.github.io/classdecl.html
chart needs a fifth color (gray). Above, I thought I had discovered
a tri-state property for {move|copy}, but there are five states,
not all of which apply to both moving and copying.
I had guessed that C++ had a deep-seated preference for copying, but
no such notion is needed to explain this. The preference isn't deep
in the language; it's right there, on the chart. It's not really a
preference at all--it's just the asymmetry between two rules:
- user-declared copy --> move not declared (not deleted)
- user-declared move --> copy deleted (not just non-declared)
It's not deep, it's shallow--it's an artifact of these complicated
backward-compatibility rules. I suppose they felt they couldn't do
anything more severe than deprecating the old implicit-copy rules.
I just wish clang or gcc had a switch to pretend that the Committee
had been more bold and overturned the implicit-copy rules.
> Also, adding move (and default, to avoid unrelated errors) ctors to "base"
> prevents the default-generated move ctor from compiling (because it calls
> the base class move ctor) and so makes the derived class non-movable:
> again, as you'd expect it to do.
Let's try that, too, to make sure I understand your intention:
---------------------------------- >8 --------------------------------------
#include <stdio.h>
#include <utility>
struct move_detector {
explicit move_detector(int n) : n_{n} {}
move_detector(move_detector const& x) : n_{x.n_}
{ printf("[%d] copied\n", n_); }
move_detector(move_detector&& x) : n_{x.n_}
{ printf("[%d] moved\n", n_); }
int n_;
};
struct base {
~base() = default;
base() = default;
base(base&&) = default;
};
struct non_movable : base {
non_movable() = default;
non_movable(non_movable const&) = default;
non_movable(non_movable&&) = default;
move_detector value{23};
};
int main() {
non_movable x;
return non_movable{std::move(x)}.value.n_;
}
---------------------------------- >8 --------------------------------------
$clang -Wall -std=c++20 eraseme.cpp && ./a.out || echo $?
eraseme.cpp:22:5: warning: explicitly defaulted copy constructor is implicitly
deleted [-Wdefaulted-function-deleted]
non_movable(non_movable const&) = default;
^
eraseme.cpp:20:22: note: copy constructor of 'non_movable' is implicitly
deleted because base class 'base' has a deleted copy constructor
struct non_movable : base {
^
eraseme.cpp:17:5: note: copy constructor is implicitly deleted because 'base'
has a user-declared move constructor
base(base&&) = default;
^
1 warning generated.
[23] moved
23
Again, it's a gray-colored box on that chart; now that I understand that,
clang's diagnostics are very clear.
> So AFAICS everything works fine here and clang -Wdefaulted-function-deleted
> is given when you'd expect it to be, i.e. when the defaulted function is
> actually deleted.
Yes. It's not a manifest error, yet, but it's better to get an early warning,
which gcc doesn't give (at least not with only '-Wall'):
$g++ -Wall -std=c++20 eraseme.cpp && ./a.out || echo $?
[23] moved
23
Thanks for helping me work through this.
- Re: [lmi] Detecting whether move semantics actually take place [Was: Install several more clang packages], (continued)
- Re: [lmi] Detecting whether move semantics actually take place [Was: Install several more clang packages], Vadim Zeitlin, 2022/07/18
- Re: [lmi] Detecting whether move semantics actually take place, Greg Chicares, 2022/07/20
- Re: [lmi] Detecting whether move semantics actually take place, Vadim Zeitlin, 2022/07/23
- Re: [lmi] Detecting whether move semantics actually take place, Greg Chicares, 2022/07/28
- Re: [lmi] Detecting whether move semantics actually take place, Vadim Zeitlin, 2022/07/28
- Re: [lmi] Detecting whether move semantics actually take place, Greg Chicares, 2022/07/30
- Re: [lmi] Detecting whether move semantics actually take place, Vadim Zeitlin, 2022/07/30
- Re: [lmi] Detecting whether move semantics actually take place, Vadim Zeitlin, 2022/07/31
- Re: [lmi] Detecting whether move semantics actually take place,
Greg Chicares <=
- Re: [lmi] Detecting whether move semantics actually take place, Vadim Zeitlin, 2022/07/31