C++ Logo

STD-PROPOSALS

Advanced search

Subject: Re: [std-proposals] Operator for reference casting
From: Bengt Gustafsson (bengt.gustafsson_at_[hidden])
Date: 2021-01-16 06:01:31


@Barry: Was the reason for shooting the proposal down that the >> was
viewed as contentious with template close brackets or were there
non-syntax related issues? It does seem that at least => or -> is free
of such risks as = or - can't end a template declaration. Actually I
can't see any real parsing issue with >> either but that's another matter.

@Arthur:

Yoe are of course right in that conceptually move and forward are
different /operations/, but as  Barry noted in his proposal
(http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0644r1.html)
forward /does /the same thing as move except for lvalue references. I
think that if we have an easy to type forward operator such as -> it
will be used also for the move operation. And this is a good thing, as
it avoids the pitfall of accidentally moving out of universal references.

The long version:

What I was trying to say was that moving a lvalue reference is not a
very common situation, most moves we do today move by value variables or
rvalue reference parameters. When we write move on a universal reference
we are often not aware that our caller's by value variables will be
moved from without their consent, which leads me to believe that the
behavior of forward is the safe choice even when we write move. Example:

struct MyType {
// Standard setter pair
void setName(string&& n) { m_name = std::move(n); }
void setName(const string& n) { m_name = n; }

     string m_name;
};

// Forwarding function
template<typename T> void set(MyType& o, T&& n) { o.setName(move(n)); }
// #1

// Use
int main()
{
     MyType o;
     string name = "Tarzan";

     set(o, name);

   cout << "My name is " << name << ".";   // #2
}

(Godbolt: https://godbolt.org/z/51czae)

At #1 we move the n parameter as we want to use the best setter.
However, this should have been /forward/. Maybe we made this function a
template at a later stage, not realizing that we thereby made our
reference universal and move incorrect.

At #2 we see the consequences, the name variable is now in the move from
state, which typically outputs as the empty string.

So in this case move should definitely have been forward. I must say
that I'm hard pressed to find any valid use case for moving out of a
lvalue reference that does not arise due to a wrongly declared
parameter- or return type:

// Non-template version of above. The type should clearly have been
string&& to indicate to caller that it will move n out.
void f(MyType& o, string& n) { o,setName(move(n)); }

// If you really want to give away your name member you should not
return it by lvalue reference, choices are between rvalue reference or
by value.
string& releaseName() { return m_name; }

// If you have a lvalue reference you got as a return value it is
implied that you are allowed to change it, and moving out is a type of
change. But when
// the intention is to offer the reference for moving by value or by
rvalue reference is a way to indicate this and by always using forward
surprises can be avoided.
string& nn = releaseName();
o.setName(std::move(nn));

// The one valid use case of moving from a lvalue reference I could find
is when you have extracted a lvalue reference to a subobject and later
decide you can
// move from it.
ComplexObject obj;
string& userName = obj[ix].nameMap["user"];   // #3
if (userName can be safely moved out)
     o.setName(std.:move(userName));

My conclusion is that as long as you have a (non-const) lvalue reference
you got from somewhere else that don't know you're going to move from it
there is either the wrong return type/parameter type or a reliance of
moved from value being the same as the empty value. The use case at #3
is valid but rare. If we didn't already have move and were contemplating
addding a forwarding operator I would say that in this case
static_cast<string&&>(userName) would be the way to go, using the
forwarding operator for all cases when it behaves as move.

Maybe there is some prominent use case of move that I didn't see?

If not it seems that with a forwarding operator we should teach to use
it for both forward and move situations, avoiding nasty surprises
arising from selecting move instead of forward for brevity. Recommending
using static_cast for the specialist case when you find the optimization
of moving from a lvalue reference into a data structure you know enough
about to be certain that this is correct. With this in mind it seems
logical to look for another name than "forwarding operator".

An alternative would be to make the forwarding operator a forwarding
only operator by making it an error to apply it to a by value variable,
forcing the use of move in this case. However this begs the question
whether we also would add a move operator, which works for by value
variables and rvalue refeerences but not for lvalue references.
Personally I think that teaching would be easier with just one operator.

Background investigations:

A little test program (Godbolt: https://godbolt.org/z/cdfqsq) shows that
in most cases move and forward actually do the same thing. Soem coments:

The direct g calls show that moving or forwarding a by value variable
has the same result. (Also including the cast variants of forwarding).
This means that if the spelling of forwarding is shortest, it will often
be used even here, even though move is the /operation/.

The perfect forwarding in the f1 function works as intended, but the
moving as in g1 is often a bug as it will allow first m1 call to move
out of my x variable without indication at the m1 call site.

The same goes for the m3 function which takes the variable in the same
way and is not often correct: If a function like m3 moves the value out
its parameter type should be like m2.

Neither in the formal proposal or in the OP it has not been made clear
what the forward operator would mean when applied to a by value
variable. As seen by the second and third call to f1 (marked #2 and #3) 
there are two possibilities, and if the forwarding operator just uses
the variable type or with as a lvalue reference. However, the choice is
clear: It should be forwarded as the by value type. This is obvious by
the fact that if a lvalue reference would result the forwarding operator
would have no effect and be useless for local by value variables. Note
that this means that the forwarding operator (#2) now works exactly as
move (#4) when applied to by value variables.

Bengt



STD-PROPOSALS list run by std-proposals-owner@lists.isocpp.org

Standard Proposals Archives on Google Groups