This is an archive of the discontinued LLVM Phabricator instance.

[libc++] Avoid using std::forward on things that aren't forwarding references. NFCI.
AbandonedPublic

Authored by ldionne on Mar 4 2022, 7:39 AM.

Details

Reviewers
Mordante
philnik
var-const
Quuxplusone
Group Reviewers
Restricted Project
Summary

Inspired by the recent Discord discussion on how to (speed up|stop generating debug symbols for) std::forward and std::move, I decided to investigate what happens if we mechanically replace all our std::forward<T>(t) with static_cast<decltype(t)>(t). In the process, I found these few places where we are using the std::forward syntax, but in fact we aren't forwarding anything — we're just using it as a more expensive/verbose static_cast. Regardless of what we do about std::forward itself, I'd like to adjust these places to reflect the fact that they're not forwarding.

In a few places you'll wonder why we do X& f(); ~~~ static_cast<X&&>(f()) instead of just X& f(); ~~~ std::move(f()). It's because sometimes X itself is an lvalue reference type, so moving-out-of its referent would be wrong, but static-casting to X&& will be a no-op.

In a few places in pair and tuple, one might be tempted to argue that we kinda are forwarding, from one member of a tuple/pair. I say sure, arguably; but IMNSHO it is vastly clearer to say static_cast<SpecificType&&>(std::get<I>(t)) than std::forward<SpecificType>(std::get<I>(t)) (when t is an lvalue reference to a tuple). Notice that in these cases, it would not be correct to write either static_cast<decltype(std::get<I>(t))>(std::get<I>(t)) or just std::get<I>(t); and also (per above) it wouldn't be correct to write std::move(std::get<I>(t)). I'm not sure if it would be correct to write std::get<I>(std::move(t)) in some cases, but I don't think that's clear either (and @Mordante has recently complained about apparent-use-after-move involving std::get, so he's probably happy not to introduce any more such cases).

After this patch, it will (at least for now) be possible to mechanically search-and-replace all std::forward<X>(y) into static_cast<decltype(y)>(y), and all the tests will still pass. I'm not saying we should land anything crazy like that, but it'll be handy to have the option for benchmarking.

Diff Detail

Event Timeline

Quuxplusone created this revision.Mar 4 2022, 7:39 AM
Herald added a project: Restricted Project. · View Herald TranscriptMar 4 2022, 7:39 AM
Quuxplusone requested review of this revision.Mar 4 2022, 7:39 AM
Herald added 1 blocking reviewer(s): Restricted Project. · View Herald TranscriptMar 4 2022, 7:39 AM

Regarding std::get<I>(std::move(tuple)), this provides a potentially relevant formal definition of when it's "okay" to do it: https://ldionne.com/2016/02/17/a-tentative-notion-of-move-independence/. In most places in this patch, my preference would be to use std::get<_I0>(std::move(__t)), since IMO that's the clearest form. In several places, we take the argument we're moving out of by value, so IMO using std::move on it makes a lot of sense.

Also -- you replaced all those places and a test failed for each of them? That's nice.

libcxx/include/__iterator/common_iterator.h
45

I don't understand why we're changing this one.

libcxx/include/__memory/unique_ptr.h
210

Can the deleter be a reference? If not, I don't understand why we're not std::moveing here (and below) instead.

libcxx/include/__utility/pair.h
224

std::move(__p).first?

libcxx/include/tuple
1591

Since the standard says this: http://eel.is/c++draft/pairs#pair-19, I think I'd rather leave it as-is. It would better if the Standard said static_cast, I think, but oh well.

Quuxplusone added inline comments.Mar 4 2022, 2:53 PM
libcxx/include/__iterator/common_iterator.h
45

Somewhat cascading drive-by. I changed the forward on line 59; then noticed that __value should be __value_ down there; then noticed that the same thing applied up here; and finally, noticed that __proxy and __postfix_proxy were actually doing the same thing, except that __postfix_proxy was misusing forward to do its static_cast<X&&>, while __proxy was "misusing" move (a trick that wouldn't even have worked in general, as your blog post says — but worked here because the programmer happened to know something about iter_reference_t<_Iter>, but then they felt guilty about the cleverness and left a comment explaining their logic. BUT, if we remove the cleverness, then we can also remove the guilty comment. The new line 45 is correct by definition, even if iter_reference_t<_Iter> were someday to become a reference type.

(That said, I'm now wondering why both of these types aren't simply aggregates!)

libcxx/include/__memory/unique_ptr.h
210

Yes, deleter_type explicitly can be an lvalue reference type.

libcxx/include/__utility/pair.h
224

That would be correct AFAICT (per your blog post), but is it really more readable, or will it lead to people asking (as in your blog post) why not std::move(__p.first) or why isn't that a double-move bug? My attitude is, if we want to cast it to precisely the type _U1&&, for whatever metaprogrammy reasons, we should just say that, instead of trying to jenga a cast out of some clever call(s) to move or forward.

IOW, arguably, static_cast<X&&> means "Look out! This is subtle for some reason!", whereas std::move doesn't set off any alarm bells. And I'm claiming that if it matters for correctness whether you've written std::move(x).y or std::move(x.y), then that is subtle by definition, and some alarm bells are warranted.

libcxx/include/tuple
1591

TBF, it doesn't use this exact expression; it says it in English.

Initializes first with arguments of types Args1... obtained by forwarding the elements of first_­args and initializes second with arguments of types Args2... obtained by forwarding the elements of second_­args. (Here, forwarding an element x of type U within a tuple object means calling std​::​forward<U>(x).)

Also, maybe you'll change your mind when you see that this is not actually the ctor pair(piecewise_construct_t, tuple<Args1...>, tuple<Args2...>) defined in the standard. This is pair(piecewise_construct_t, tuple<_Args1...>&, tuple<_Args2...>&, __tuple_indices<_I1...>, __tuple_indices<_I2...>). So we've already messed with the types of first_args and second_args by ref-qualifying them. (Mind you, that doesn't change their value categories as far as the paragraph above is concerned.)

I strongly prefer not to use std::forward here, because std::forward<_Args1>(std::get<_I1>(__first_args)) is not equivalent to static_cast<decltype(std::get<_I1>(__first_args))&&>(std::get<_I1>(__first_args)), which means I can't (locally) turn it into _LIBCPP_FWD(std::get<_I1>(__first_args)). I want to permanently burninate any use of std::forward<X>(y) that can't be turned into _LIBCPP_FWD(y), because (by my definition at least) that is not forwarding.

Per your blog post, this is another place where we could use get<I>(move(x)):

:  first(std::get<_I1>(std::move(__first_args))...),

Physically correct, readability-wise-scary to me. But still, if you forced me to choose between forward<X>(get<I> or get<I>(move(, I would choose get<I>(move(. Because get<I>(move( is subtle, but forward<X>(get<I>( is just a flat-out lie AFAIC. It's not forwarding!

ldionne commandeered this revision.Sep 5 2023, 12:57 PM
ldionne edited reviewers, added: Quuxplusone; removed: ldionne.

[Github PRs transition cleanup]

I think the benefit of this isn't sufficient to justify going through the trouble of rebasing and figuring out what else needs to change, so I'll abandon this one.

ldionne abandoned this revision.Sep 5 2023, 12:57 PM