Document number: xxx1
Date: 2023-06-18
Audience: EWG
Reply-To: Mihail Naydenov <mihailnajdenov at gmail dot com>


Uniform ‘move’ and ‘forward’ syntax

1 Abstract

This paper proposes unifying both std::forward and std::move behind a single postscript operator &&.

2 Demo

Forward without extreme verbosity:

auto cally = [](auto&& func, auto&&... y)  
{
  return std::invoke(func&&, args&&...); 
}

Move with more natural syntax

Class::Class(string&& name, function&& func)
  : _name(name&&), _func(func&&) //< usage visually matches declaration
{}

Uniform, “no-brainer” use of either forward or move

decltype(auto) insert(string&& name, auto&& func)  
{
  return acts.emplace(name&&, func&&); //< move or forward? Does not matter!
}

3 Motivation

3.1 Motivation for uniformity

In the decade of use of move and forward few observation can be made:
- One uses either move or forward. Using one in the place of the other is in practice always wrong;
- The results of the operations are effectively the same - object is “passed along” and its identifier should be consider “a shell” (potentially in moved-from state);

The first observation lead us to the conclusion, the correct use of each function is statically deterministic and not bound to any a decision making on the part of the programmer.
The second observation tell us, there is an underlying concept, connecting both operations to a common base - the concept of passing-along and giving up usage rights over an object.

These two let us to believe, it is possible to come up with a more general operation, one which combines the currently used two separate operations. Doing so will have multiple positive benefits:

  • Less fragile code
decltype(auto) insert(string&& name, auto&& func)  
{
  return actions.emplace(std::move(name), std::forward<F>(func)); 
}

In the above example, if the user decides to “upgrade” the function to make the name argument a templated one, he/she must remember to also change move to forward or risk either confusion for the people reading the code or even unexpected behavior.

With the current proposal the user (or tools), can change the function signature and the code will still behave correctly in all cases.

  • Some cases are tricky
template<class... T>
class Wrapper  
{
  decltype(auto) operator()(T&&... val) //< not a forwarding reference
  {
    return call_stub(std::forward<T>(val)...); //< Correct?
  }
}

The decision to use either move or forward is not always that obvious. It would be nice to have an operation that “just works”.

  • One less thing to learn and remember

The distinction b/w forward and move is subtle, arguably it is an “intermediate-to-advanced understanding”. However, this “understanding” alone does not contribute to writing “better”, more expressive code.
The tools to write better code are rvalue references and forwarding references, not the fact separate functions are needed to use each one! In other words, right now one must learn three topics:
- Forwarding references and reference collapsing;
- Rvalue references and move constructors;
- Learn about move and forward, when to use them and when not to use them1;

From the above three, only the first two are fundamental concepts of the language. The last ones are helper constructs.

With the current proposal focus is left on the fundamental features and away from the helper tools. These tools are brought to the absolute minimum both semantically and syntactically. They are essentially left to the compiler to decide, as it already knows the aswer.

  • The compiler should be the one writing boilerplate code

Considering that, on one hand, there is no decision making on when to use move and when forward, this is always predetermined, and, on the other, the compiler has all the necessary information to do the right choice - there is little to no gain from not letting it do the job for us.

This proposal considers move and forward boilerplate (helper tools), which should be streamlined by the compiler as much as possible.

  • Higher level abstractions are better, if the lower ones do not grant us more abilities

Let’s consider a higher abstraction function in the form of a destructor or a dispose() method, against specific implementations in the form of some hypothetical image.freeData() or file.close(). In almost, if not all cases the destructor or dispose() is the better interface, because, beyond explicitness, one gains nothing from using the specific methods and loses more in the form of less generic code.

This proposal argues the case here is analogous - the use of specific implementations, in the form of forward and move, is inferior to the use of the higher concept of “object passing”.

  • Any good operator for move is also good one for forward

This might look like a cop out, but in reality if a good operator is found for one of the functions it will either be also equally useful for the other, or it will be very hard to come up with similar one for the other function as well. This goes the opposite direction as well - as long as we are not forced to came up with a specific operator, it will be easier to actually came up with one! This comes down to the fact, both operations stemming from a common base concept, as already mentioned.

This proposal recognizes the strong connection b/w move and forward and considers a single operator the best solution.

3.2 Status Quo

3.2.1 The case of std::move

The biggest problem with move is its misleading similarity with actual standard library functions, in particular std::copy. Any outside observer will be mislead to think both those functions are complimentary to one other!
Being so explicit in what the function states it does work to its decrement as move might or might not move, in contrast to functions in general (imagine copy copping “sometimes”). Further, even if does move, it will not be really moving something in the sense of a function being executed and an operation being observable in a debugger. A function is not a great abstraction for this kind of operation.

3.2.2 The case of std::forward

forward is much more problematic and has been already a target for debate and previous proposals2. Here is the list the problems:

  • Subtle differences with move leads to uncertainly in novice programmers.
  • The function is used only in one context and that context alone - to pass-on a “forwarding reference”. This is not a good practice.
  • Mandatory template argument is unexpected to even intermediate programmers, especially when they are already introduced to move. Mandatory explicit template arguments are not common in general and in some situations are even an anti-pattern.
  • The template argument itself can be confusing for what works and what does not. Should/could type decorations be used? Should/could a different type be used? When? Why?
  • For experts the main issue is the extreme verbosity of the entire expression (long function name + template argument) in the already verbose context of templates where it is used. If forwarding is not used in a explicit template context, it is used in a forwarding lambda. In that case the lambda expression becomes dominated by the forwarding.

The last issue is more important then it sounds, because it leads to experts not using forward at all!
They either use a FWD macro (as mentioned in p0644), or more recently3 just do a C-style cast, using (Type&&)value expression!
If experts do not use forward, how can we expect regular programmes to do?!? And in fact, they don’t! Because in many if not most cases nothing will really happen if someone forgets to add forward, people often forget to added it and/or prefer the prettier and leaner code without the forward verbosity. (Please note, this is regular code, not library code we are talking about, and indeed very often adding forward makes no difference, because most often arguments to functions are non-rvalue references.)

If both experts and regular users avoid forward, we have an issue that should be addressed.

3.2.3 Uniform use

The mentioned std::execution (P2300R7) is very interesting because of code like this:

class inline_scheduler {
  struct _sender {
    template <class R>
    friend auto tag_invoke(std::execution::connect_t, _sender, R&& rec)
      noexcept(std::is_nothrow_constructible_v<std::remove_cvref_t<R>, R>)
      -> _op<std::remove_cvref_t<R>> {
      return {(R&&) rec};
    }
    ...
  };

  template <class R>
  struct _op {
    [[no_unique_address]] R rec_;
    friend void tag_invoke(std::execution::start_t, _op& op) noexcept {
      std::execution::set_value((R&&) op.rec_);
    }
  };
  ...
};

This shows the already mentioned c-style cast, but something else is more interesting.
The cast is applied for both forward and move. So, in the end, we even have “established practice” of uniform use.
It must be stressed, this is not some random code from the internet, from some random developer, trying to be clever. This is highest possible expert-level code, intended to be viewed exclusively by experts, part of the C++ committee itself!

If experts write code like this, why we should expect regular users to settle with the verbosity?
It is dishonest in the “Do as I told you, not as I do” kind of way.

4 Details

4.1 Postscript operator &&

This proposal considers best solution the introduction of a postscript operator &&, which, when applied to a value or reference val will be equivalent of std::forward<decltype(val)>(val). As a result the variable is either reference-collapsed (“forwarded”), or casted to rvalue reference (“moved”), covering all uses of both forward and move.

The syntax, is specifically chosen as it is visually reminiscent of both rvalue reverences (std::string&&) and “forwarding references” T&&, auto&&.
This way there is a strong correlation b/w the decoration of a variable and the action needed to use it. Even in the case where we move a value, not a reference (A a; a&&), this corelation is preserved, because the type of the a&& is A&&, creating symmetry - given a of type A, applying &&, we get a&& of type A&&.

The operator being postfix makes the code readable in the natural left to right order:

Image img;
img&&.mirrored();  //< std::move(img).mirrored()

std::optional<S> o;
auto taken = *o&&;  //< *std::move(o)

[](auto&& arg) {
  func(arg&&); //< func(std::forward<decltype(arg)>(arg));
}

class A {
  Some s;
  decltype(auto) func(this A&& that) { return that&&.s; } //< std::move(that).s
};  

As you can see, the flow of the expressions is undisturbed, compared to what we have today.

Should be noted, some code will be ambiguous with the binary operator&&:

a&&(b || c); 

This could either be

  • operator&&(a, operator||(b, c))
  • std::move(a).operator()(operator||(b, c))

To avoid braking existing code, proposed is the new operator to have lower precedence.
All code today, that uses &&, will continue to work as it is. The above will result in binary operator &&.
To get the move behavior, one must surround the expression with brackets:

(a&&)(b || c); //< std::move(a).operator()(operator||(b, c))

In reality, most generic code will use std::invoke instead, making this corner case even narrower.

4.2 Can operator && be a standard operator function?

No.
move can be an operator function, but forward needs a type argument, making it unsuitable. This proposal envisions the compiler supplying the needed template argument “behind the scenes”.

4.3 Should operator && be overloadable?

No.
We want it have the predictable behavior in every context, including generic code like templates.

4.4 Should we use a special function instead?

No, may reasons against such a course of action:

  1. Both move and forward are interfaces to fundamental, language constructs. They should be represented by language-level syntax. Right now, the much less common operation, to take the address of an object, has a language level syntax (&), and the much more commonly used one, to pass an object, does not! There is also in strong contrast with the fact the entire machinery, used by move and forward is language based (move ctors, decorators, reference collapsing) yet there no language construct to use them!
  2. Currently we need to explain that “move does not actually move” and that “move and forward are just a cast”. This is because move and forward present themselves as functions, similar to other std functions like std::copy and std::ref, which, like most functions, do perform a “real work”! Casts on the other hand are language level actions, performing essentially compiler directives, not “real work”. Operators fit that purpose best.
  3. An operator has the least amount of verbosity, which is welcome for both move and forward as in both cases the operation is already part of some bigger expression - func(std::forward<A>(a), std::forward<B>(b)), _member{std::move(val)}, std::move(val).something().

4.5 Should we use a keyword instead?

No.

First, it will not be short and terse enough to be attractive alternative to the current functions, especially to move.

Second and much more importantly, inviting a keyword for both move and forward will have the unwanted side effect of introducing a new term, a new overarching category to articulate. Instead of just talking about “forwarding references” and rvalue references, we will have to talk about object give-ing or pass-ing as some sort of separate category. Keywords, even more so then functions, make things way too “official” and declarative, where operators are just syntax - we don’t name what we don’t have to name. Where with new and make_ naming the action is beneficial, here naming the action like give or pass is overly formal and, as said, creates an extra unneeded term that combines the two actions “officially” into a new, third one, with a different name. We don’t need that.

Last, but not least, introducing a new keyword to C++ has proven to be very, very, very, very, very difficult task and the results are less then ideal.

4.6 Should std::move and std::forward be deprecated?

No.
This proposal anticipates, move and forward to still be used by people who like to be explicit in the syntax, much the same way some people use or and not instead of || and !. More importantly, forward can be used to forward an argument as a different type and move can be used where the user wants to force move, avoiding reference collapsing of a forwarding reference.


  1. guide to std::forward and std::move.↩︎

  2. Forward without ‘forward’ (p0644r1).↩︎

  3. std::execution (P2300R7)↩︎