martinboehme wrote:

> @martinboehme Could you explain how that is any different from this?
> 
> ```
> #include <string>
> #include <vector>
> 
> bool TryInsert(std::vector<std::string>& v, std::string&& s) {
>       if (some_condition()) { return false; }
>       v.push_back(std::move(s));
>       return true;
> }
> 
> std::string foo(std::vector<std::string>& v, std::string&& s) {
>       if (!TryInsert(v, std::move(s))) {
>               return std::move(s);  // warning: 's' used after it was moved 
> [bugprone-use-after-move]
>       }
>       return {};
> }
> ```

You mean how is this different from the example I posted earlier? Quoting it 
again:

```cxx
SomeMoveableType obj;
void foo(SomeMoveableType &&, SomeOtherType);
// Obviously a contrived example, but you get the point
foo(std::move(obj), obj.operation());
```

One difference is that `TryInsert` in your example has "maybe move" semantics 
(it might move, or it might not, similar to `try_emplace()`). But that's not 
the decisive difference here.

The key line in my example is this one:

```cxx
foo(std::move(obj), obj.operation());
```

Note how this uses `obj` in two arguments to the same function.

If the check assumed that `std::move(obj)` already leaves `obj` in a moved-from 
state, then it would need to flag `obj.operation()` as a use-after-move. But 
the check knows that the move only happens once `foo()` starts executing, so 
`obj.operation()` is guaranteed to be evaluated before the move, and hence it 
is not a use-after-move[^1].

Now let's look at a similar example but with a user-defined invalidation 
function. We'll assume 
[`close(2)`](https://pubs.opengroup.org/onlinepubs/009604499/functions/close.html)
 has been marked as an invalidation function.

```cxx
int fd = open(...);
void foo(int err, ssize_t bytes_written);
foo(close(fd), write(fd, ...));
```

Observations:

- This is a synthetic example -- I'm not sure what purpose this would serve. 
But I assume this kind of pattern could come up in real code with other 
invalidation functions.
- This is an "use after invalidation". The order in which function arguments 
are evaluated is unspecified, so we have to assume pessimistically that 
`close(fd)` (the invalidation) is evaluated before `write(fd, ...)` (the use).
- The check does not currently flag this use after invalidation because it uses 
the same logic as for `std::move` and hence concludes, erroneously, that the 
invalidation only happens once `foo()` starts executing.

> It seems to me the crux of your argument...
> 
> > sooner or later someone is going to ~configure an invalidation function~ 
> > _compose an outer function_ that does return a value, will use that value 
> > in an expression, and be mightily confused about why the check doesn't 
> > understand when the invalidation happens.
> 
> ...applies ~verbatim to situations with `std::move` and `std::forward` too, 
> no?

I'm not sure exactly what you mean here (particularly with respect to the 
"compose an outer function"), but I hope I've made it clear above how 
`std::move` and `std::forward` are different from user-defined invalidation 
functions.

> > I realize this is often not an issue in practice
> 
> This is important, and has been our guiding principle here. The reality is 
> this check (and the typical tidy in general) has always had rough edges and 
> been inherently limited in what it can do. 

Definitely. (I'm the original author of bugprone-use-after-move and am 
painfully aware of its limitations.)

However, we accept these limitations because they are hard to overcome. The 
issue I point out above is not hard to overcome at all: `std::move` and 
`std::forward` should continue to determine the `moving-call` using [this 
matcher](https://github.com/llvm/llvm-project/blob/a95a9465bfd261f7b7a464d79336faa74af780c0/clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.cpp#L508),
 whereas user-defined invalidation functions should simply consider the 
`moving-call` to be identical to the `call-move`[^2]. (IOW, we should be doing 
_less_ work for user-defined invalidation functions!)

My wider point is that this illustrates how the semantics of `std::move` and 
`std::forward` are different from user-defined invalidation functions and that 
these should therefore be handled by a different check (which would internally 
reuse almost all of the implementation).

> All that said, though, I'm not really opposing you: if you'd like to make the 
> check more precise, or rename it more accurately, and others are on board -- 
> I have no particular opposition. I've done what I can to improve it so far, 
> and I'll be happy to see contributors improve the check further.

I unfortunately currently can't devote the time to contributing to the check. I 
realize it's on me that I didn't notice this PR while it was open and voice my 
concerns at the time. I do believe that the status quo is unsatisfactory, and I 
hope that I've explained why.

[^1]: C++ doesn't define the order in which function arguments are evaluated; 
if `obj.operation()` is evaluated before `std::move(obj)`, there's no question 
of this being a use-after-move of course. But even in the case where we get 
"unlucky" and the `std::move(obj)` is evaluated first, this isn't a 
use-after-move.

[^2]: Note also how these nodes are now misleadingly named. They should really 
be `invalidating-call` and `call-invalidator` or similar.

https://github.com/llvm/llvm-project/pull/170346
_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to