Don’t forward
things that aren’t forwarding references
forward
things that aren’t forwarding referencesSome readers of yesterday’s post
(“A deduction guide for Foo(Ts..., Ts...)
”, 2023-05-26)
may wonder about the part where I wrote:
template<class... Ts>
class Foo {
public:
explicit Foo(Ts... as, Ts... bs)
: a_(static_cast<Ts&&>(as)...),
b_(static_cast<Ts&&>(bs)...) {}
private:
std::tuple<Ts...> a_;
std::tuple<Ts...> b_;
};
Since we all know that std::forward<T>(t)
is synonymous with static_cast<T&&>(t)
,
why did I choose static_cast
above instead of writing std::forward<Ts>(as)...
?
The answer is not simply that I prefer core-language features over library facilities,
and it’s not that I’m worried about forward
’s compile-time performance.
Now, it’s true that the Standard Library could get along fine without std::forward
: Whenever
you use std::forward<T>(t)
on a forwarding reference T&& t
, it’s always doing exactly
the same thing as static_cast<T&&>(t)
. One could rewrite all one’s std::forward
s
as casts. But we generally don’t do this, because the specific spelling std::forward
gives the reader useful information: “I’m forwarding this forwarding-reference parameter!”
It’s greppable, and somewhat self-explanatory — much more so than a cast, anyway. We use
std::forward
when we are, semantically, doing perfect forwarding.
If you use std::forward
only for perfect-forwarding, then I can take your program
and mechanically search-and-replace every instance of std::forward<T>(t)
into
decltype(t)(t)
, and it’ll still compile and do exactly the same thing. I’ve actually
done this for libc++, by search-and-replacing std::forward<T>(x)
into _LIBCPP_FWD(x)
and then compiling with various macro-definitions of _LIBCPP_FWD
. Of course this works
only if you never do anything “weird” via std::forward
. If you shoehorn it in places
where it is physically correct but semantically incorrect, this mechanical transformation
won’t work at all. For example, if you write:
std::unique_ptr<int> oops() {
auto p = std::make_unique<int>(42);
return std::forward<std::unique_ptr<int>>(p);
}
This return
statement is physically valid: It takes the lvalue p
, abuses
std::forward
to cast it to unique_ptr&&
, and then constructs the return slot
using unique_ptr
’s move-constructor. But it’s not semantically correct: We
aren’t doing perfect forwarding here, so we shouldn’t use std::forward
. In fact
what we meant was simply
return p;
In my experience, programmers new to move semantics will tend to sprinkle their
code with std::move
and std::forward
until it “works,” ending up with a program
that doesn’t look nearly as clean as it should, and works by accident, if at all.
(Programmers new to pointers will do the same with *
and &
.) It helps immensely
if we can give simple concrete rules for when to use move
and forward
, and when not to.
-
Use
std::move(x)
on the last use of an objectx
, to signify your willingness to transfer ownership of that object’s resources. Otherwise, don’t usemove
. -
Use
std::forward<X>(x)
when perfect-forwarding a function parameter of forwarding-reference typeX&&
. (This, also, is a transfer of ownership, and must be the last use ofx
.) Otherwise, don’t useforward
.
Just as the guideline against “pass-by-const-value” allows us to quickly detect that there’s a bug in
auto plus(const std::string s, const std::string& t) {
return s + t;
}
the guideline against “forwarding anything that’s not a forwarding reference” allows us to quickly detect that there’s some kind of mistake in
template<class T>
void bar(std::vector<T>& c, int i) {
foo(std::forward<T>(c[i]));
}
(a snippet seen in a real std-proposals thread from 2018). We might not know
quite what the bug is, yet; but we know something’s wrong, because the programmer
is using forward
where no forwarding reference exists.
The flip side — the price I must pay to gain this amazing bug-finding benefit — is that
I mustn’t write forward
where no forwarding reference exists. Not even when, as in
yesterday’s post, it
contingently happens to work out
to the right physical behavior in all cases. In those situations, I avoid forward
.
Use
std::forward
only when perfect-forwarding.
See also:
- “‘Universal reference’ or ‘forwarding reference’?” (2022-02-02)
- “Benchmarking Clang’s
-fbuiltin-std-forward
” (2022-12-24)