lmi
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [lmi] Detecting whether move semantics actually take place


From: Vadim Zeitlin
Subject: Re: [lmi] Detecting whether move semantics actually take place
Date: Sat, 23 Jul 2022 17:41:36 +0200

On Thu, 21 Jul 2022 00:53:01 +0000 Greg Chicares <gchicares@sbcglobal.net> 
wrote:

GC> On 7/18/22 16:19, Vadim Zeitlin wrote:
GC> > On Mon, 18 Jul 2022 15:48:14 +0000 Greg Chicares 
<gchicares@sbcglobal.net> wrote:
GC> [...]
GC> > GC> Let me pose a hypothetical question first: does this line
GC> > GC>      db = std::move(da);   // D& operator=(D&&)
GC> > GC> actually perform a move and not a copy, in a context where
GC> > GC> class D defaults all special member functions?
GC> > 
GC> >  If the defaulted move assignment operator actually exists and is not
GC> > deleted, then this will perform a move, yes.
GC> 
GC> The question I really want to ask is: is it possible in C++20 to answer
GC> that question for a different class E of which we know nothing?

 I thought it would be, and now you've conclusively proved that it can, by
actually doing it, but I'm still not sure about how are you going to use
it.

GC> >  If you wanted to ensure that a copy doesn't happen here, you'd have to
GC> > delete the (usual) assignment operator. But if you want to allow moving
GC> > without requiring it, I indeed don't see any way to ascertain that the
GC> > value is moved.

 I.e. now we know that we can have is_effectively_moveable<T> (for
simplicity, I'm not distinguishing between move-constructible and
move-assignable neither here nor below), but how are you going to use it,
exactly? You could static_assert that it's true, but where would doing this
be useful?

GC> Here it's important to explain why I'm asking this question.
GC> 
GC> The number of classes in lmi is very approximately five hundred:
GC>   $git grep '^\<class\>' *.?pp |wc -l
GC>   527

<completely gratuitous aside>

 To make this slightly more precise we could at least avoid counting the
forward declarations:

        % git grep '^class [^;]+$' *.?pp | wc -l
        346

BTW, while looking at this I've only tried using "git grep --count" and
while this didn't work for giving the total number of matches, I've noticed
a pretty unusual distribution of number of classes defined per file:

        % git grep --count '^class [^;]+$' *.?pp | sort -k2 -nr -t: | head -n10
        pdf_command_wx.cpp:31
        census_view.cpp:14
        tn_range_type_trammels.hpp:9
        ihs_server7702.hpp:7
        database_view_editor.cpp:7
        datum_sequence.hpp:6
        pcre_regex.hpp:5
        multidimgrid_safe.hpp:5
        html.hpp:5
        alert.cpp:5

i.e. my pdf_command_wx.cpp is truly an outlier and only 26 files have more
than 2 classes in it.

</aside>

GC> Suppose lmi copies an object, and I want to consider moving it instead.
GC> I can write the move operation, but will move semantics actually take
GC> place, or will the compiler silently substitute a copy to fulfill my
GC> move request? I don't simply want to make the request; I want to know
GC> how it's fulfilled. If moving is inherently impossible for some class,
GC> I'd like to know that before I waste time trying to move it.

 I think the simplest way of answering this question is to declare the move
constructor as default and rely on clang -Wdefaulted-function-deleted to
tell you if it turns out that your move ctor is implicitly deleted. Isn't
it?

GC> Similarly,
GC> I'd like to know whether moving is only incidentally impossible: e.g.,
GC>  - because I've made the move functions inaccessible; or
GC>  - because I've deleted them for no good reason, just because I didn't
GC>      want to spend time considering them; or
GC>  - because I made a base class unmoveable for some such reason; or
GC>  - because I casually added a 'const&' data member for no strong
GC>      reason, rendering the class unassignable;
GC> because then I might want to consider removing that incidental cause.

 Note that is_effectively_moveable<T> doesn't actually help answering these
questions. But both the clang warning and is_effectively_moveable concept
(if you defined it, see below) do because they provide additional context
for the error they generate. E.g., just for the illustration purposes, here
is an example of a clang warning in my own simplified test case

---------------------------------- >8 --------------------------------------
effectively_movable.cpp:44:5: warning: explicitly defaulted move constructor is 
implicitly deleted [-Wdefaulted-function-deleted]
    movable_by_copy(movable_by_copy&&) = default;
    ^
effectively_movable.cpp:41:26: note: move constructor of 'movable_by_copy' is 
implicitly deleted because base class 'not_movable' has a deleted move 
constructor
struct movable_by_copy : not_movable {
                         ^
effectively_movable.cpp:38:5: note: 'not_movable' has been explicitly marked 
deleted here
    not_movable(not_movable&&) = delete;
    ^
---------------------------------- >8 --------------------------------------


GC> If I could write
GC>   static_assert(move_will_actually_use_move_semantics<T>);
GC> then I could save that waste of time (or consider remediating the
GC> reason why such an assertion fails).

 So now we know that you can do it. But does this still seem like a better
solution than just checking clang warning to you?

GC> We have
GC>   std::is_move_constructible
GC>   std::is_move_assignable
GC> but they don't perform this job: they say that I can request a move,
GC> but not that I can actually get move semantics.
GC> 
GC> Searching the web for an answer leads me to believe that most people
GC> think the 'trait' I want cannot be implemented. I've tried to do it
GC> anyway:
GC>   https://git.savannah.nongnu.org/cgit/lmi.git/log/?h=odd/move_semantics
GC> and I'd like to know what you think, because this is tricky stuff and
GC> I might simply be deceiving myself.

 I've postponed replying to this message because I wanted to give me time
to think about it, but even after doing it for almost 2 days, I don't see
anything wrong with your solution.

GC> The technique uses a template class:
GC> 
GC>   template<typename T>
GC>   struct ambiguator
GC>   {
GC>       operator T const&();
GC>       operator T&&();
GC>   };
GC> 
GC> such that
GC>   T t {ambiguator<T>{}};
GC> would be a compile-time error if both copy and move constructors are
GC> candidates in overload resolution.

 This is an ingenuous solution, I don't know if I would have found it if
you hadn't given it to me first. And, again, I can't find anything wrong
with it.

 Just to record this here: I had my doubts about whether this worked with
move-only classes initially, but it does, because even if the copy ctor is
deleted, it still participates in the overload resolution, and so still
makes constructing T from ambiguator<T> ambiguous. And copy ctor can't be
"implicitly deleted" by just not providing it, unlike the move one.

GC> The tests on that branch attempt to establish that the longed-for
GC>   move_will_actually_use_move_semantics<T>
GC> trait can be implemented as
GC>   equiplausibly_constructible<T> && std::is_move_constructible<T>
GC> and similarly for move assignment. Or have I only found fool's gold?

 I think you did manage to find the solution to the original problem, but
I'm just not sure if it's the right problem to solve. If you think it is,
I'd like to mention a couple of improvements:

 First, and trivially, you should sprinkle magic constexpr dust all over
your code, i.e. declare ambiguous conversion operators and all the
functions (equiplausible_construct, equiplausible_assign, c_moveable,
a_moveable) as constexpr, as this is needed to allow using static_assert<>
with them. You could also make sandbox_test a compile-only test if you
replaced all LMI_TEST() in it with static_assert<>s.

 Second, I think this would work even better with concepts rather than
simple template functions, both because the former are slightly more
readable (even if the functions here are pretty clear too) and because they
result in much better error messages, similar to the clang warning above,
except that this also works with gcc, if the requirement fails. Here is my
simple concept-based test:

---------------------------------- >8 --------------------------------------
#include <concepts>

template<typename T>
struct ambiguator {
    constexpr operator T const&();
    constexpr operator T&&();
};

template <typename T>
concept is_effectively_moveable =
    std::move_constructible<T> && !std::constructible_from<T, ambiguator<T>>;

... trivial struct definitions snipped for brevity ...

static_assert( !is_effectively_moveable<not_movable> );
static_assert( !is_effectively_moveable<movable_by_copy> );
static_assert( is_effectively_moveable<really_movable> );
static_assert( is_effectively_moveable<only_movable> );
---------------------------------- >8 --------------------------------------

 Please let me know if you'd like to have a patch changing sandbox_test.cpp
to use concepts, it should be pretty simple to do but I haven't done it yet
because I wasn't sure if it was worth it -- because I still think that just
checking for clang warnings would be sufficient to address you concerns
about the class not being effectively moved.

 Regards,
VZ

Attachment: pgpf7sP9mSsj4.pgp
Description: PGP signature


reply via email to

[Prev in Thread] Current Thread [Next in Thread]