3
struct test {
    test& foo(std::vector<int> const& v) {
        assert(v.size() == 1); return *this; 
    }

    void bar(std::vector<int> v) {}
};

void do_test() {
    std::vector<int> v = { 42 };
    return test{}.foo(v).bar(std::move(v)); // <-- here
}

clang-tidy gives an error bugprone-use-after-move on "here" string

Why it is use after move?

vladon
  • 8,158
  • 2
  • 47
  • 91

2 Answers2

4

Since C++17 it is guaranteed that the postfix-expression in a function call, i.e. here test{}.foo(v).bar is evaluated before any expression in the argument list.

Therefore the move-construction will happen only after foo has returned.

However, before C++17 there was no such guarantee and the move-construction might have happened before the call to foo.

It is questionable whether it is a good idea to rely on these new guarantees. Some tools tend to ignore them and continue warning about such code since it would be dangerous if compiled with a different -std= flag. GCC does for example the same with its -Wunsequenced warnings. There is also the risk that some older compiler versions may still have had bugs in that regard when C++17 was just implemented.

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • 1
    But this is not _one_ function call, no? First `foo` is evaluated, returning a reference to `this` and that reference is used for a second function call `bar`. `foo` has to return for that reference return value to be set, not? I mean, the optimizer might reorder stuff, but The sequential calls of first `foo` then `bar` must be guaranteed, right? I don't know the standard, but this seems to basic to be unreliable. And I though we could assume arguments are not evaluated until the function is called. – JHBonarius Sep 15 '22 at 11:30
  • @JHBonarius Yes, `bar` is guaranteed to be called after `foo` in any C++ version, but that doesn't mean that the constructor for `bar`'s parameter will not be called before both of them. The parameter construction happens in the context of the function containing the `test{}.foo(v).bar(std::move(v));` expression, not as part of the `bar` function call. – user17732522 Sep 15 '22 at 12:08
  • @JHBonarius "_And I though we could assume arguments are not evaluated until the function is called._": That is not the case. Even in current C++ it is not guaranteed that argument expressions are evaluated immediately before the function is called. For example in `f1().g1(h1()) + f2().g2(h2())` the compiler can still call the functions for example in the order `f1 -> f2 -> h1 -> h2 -> g1 -> g2`. – user17732522 Sep 15 '22 at 12:38
  • But at least it is now guaranteed that `h1` comes after `f1` and `h2` after `f2`. That wasn't the case before C++17. As mentioned in the [paper](https://wg21.link/p0145r3) introducing the new guarantee that was really counter-intuitive and something even experts would get wrong in practice (even if they are aware of the rules). – user17732522 Sep 15 '22 at 12:38
  • Wow, that's just nightmare fuel. Thanks for the extended explanation – JHBonarius Sep 15 '22 at 13:31
1

In

foo(v).bar(std::move(v))

There are no guarantees(See comments, pre C++17) that the v in foo(v) will be evaluated before the std::move(v) in bar(std::move(v))

The clang-tidy docs comes with an example of this.

Unsequenced moves, uses, and reinitializations

In many cases, C++ does not make any guarantees about the order in which sub-expressions of a statement are evaluated. This means that in code like the following, it is not guaranteed whether the use will happen before or after the move:

void f(int i, std::vector v); std::vector v = { 1, 2, 3 }; f(v[1], std::move(v));

In this kind of situation, the check will note that the use and move are unsequenced.

The check will also take sequencing rules into account when reinitializations occur in the same statement as moves or uses. A reinitialization is only considered to reinitialize a variable if it is guaranteed to be evaluated after the move and before the use.

Captain Giraffe
  • 14,407
  • 6
  • 39
  • 67
  • 2
    The rule changed with C++17 for this way of calling functions by member access. Actually a very similar example that had gone unnoticed in Stroustrup's book for a long time was part of the reason for that change, see https://open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0145r3.pdf. – user17732522 Sep 15 '22 at 11:09
  • 2
    But `f(v[1], std::move(v));` is not the same as `foo(v).bar(std::move(v))`. In the latter, the return value of `foo` has to be evaluated before `bar` may run. These are sequential statements, coupled together. – JHBonarius Sep 15 '22 at 11:23
  • 1
    @JHBonarius Indeed, but the argument has to be evaluated before the `bar` call. – Captain Giraffe Sep 15 '22 at 11:31
  • 1
    Yes, I understand, but not before the `foo` call right? I mean, we should have some guarantees there? I understand the possible out-of-order issue with a _single_ function call, but these are two different functions. – JHBonarius Sep 15 '22 at 11:33
  • @JHBonarius My understanding is that pre 17, no guarantees would be made. See also the samples in user17732522 link. They are also enlightening to the issue. – Captain Giraffe Sep 15 '22 at 11:38