-
Notifications
You must be signed in to change notification settings - Fork 13.4k
[clang-tidy] bugprone-use-after-move - false-negative #90174
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
@llvm/issue-subscribers-clang-tidy Author: Piotr Zegar (PiotrZSL)
Example:
No issue from bugprone-use-after-move or from clang-analyzer-cplusplus.Move.
Detected by clang-analyzer-cplusplus.Move, but not by bugprone-use-after-move |
@llvm/issue-subscribers-clang-static-analyzer Author: Piotr Zegar (PiotrZSL)
Example:
No issue from bugprone-use-after-move or from clang-analyzer-cplusplus.Move.
Detected by clang-analyzer-cplusplus.Move, but not by bugprone-use-after-move |
+ |
I can confirm that the first case is not reported by CSA, because we don't inline If we were inlining #include <memory>
struct A {};
struct SinkA {
SinkA(std::unique_ptr<A>);
};
template <typename T, typename... Args>
auto my_make_unique(Args&&... args){
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
void natively(std::unique_ptr<A> x) {
std::unique_ptr<A> tmp = std::move(x);
std::unique_ptr<SinkA> y2{new SinkA(std::move(x))}; // reported
}
void viaStdMakeUnique(std::unique_ptr<A> x) {
std::unique_ptr<A> tmp = std::move(x);
std::unique_ptr<SinkA> y2 = std::make_unique<SinkA>(std::move(x)); // not inlined, escapes...
}
void viaMyMakeUnique(std::unique_ptr<A> x) {
std::unique_ptr<A> tmp = std::move(x);
std::unique_ptr<SinkA> y2 = my_make_unique<SinkA>(std::move(x)); // reported
} We should have a way to enforce certain functions to be always inlined, such as In this sense, this relates to #81734, #64584 and #94193. In other cases, we actually want the opposite: ensure that certain functions are never inlined, like |
@steakhal inlining is an compiler optimization, it does not exist on AST level. For me this is just bug, we got second std::move, but check is ignoring it. |
Yes, but I was refering to inlining in the static analyzer domain. Sorry for the confusion. Wrt the second example and clang-tidy: thats outside of my domain. I dont want to comment on that. |
Problem is that bugprone-use-after-move incorrectly designed. Check is trying to be smart and figure out if std::move actually resulted in move or not. And due to that it does not handle most of scenarios like try_emplace and this here. Where check should be simply straight forward and flag any second std::move that occur after first one. |
- Removed custom smart pointers handling (were hidding issues) - Changed 'move occurred here' note location to always point to 'std::move' - Fixed issue when 'std::move' of 'x' on initialization list would be incorrectly detected as use after move. Closes llvm#90174
Both of the examples in this bug use The behavior of both code examples in this bug is well-defined and may well be what the programmer intended. I don't think, therefore, that we should be emitting a diagnosis on these examples. Do you get use-after-move warnings if you substitute
I'm not sure exactly what you're referring to -- can you elaborate? The check doesn't try to figure out whether void maybeConsume(std::vector<int> &&);
void f() {
std::vector<int> v;
maybeConsume(std::move(v));
} Does the call to What I think you may be referring to is that the check considers the move to happen not in the This distinction is important when sequencing rules come into play. Consider this example (from the lit test): void passByRvalueReference(int i, A &&a);
void sequencingOfMoveAndUse() {
// This case is fine because the move only happens inside
// passByRvalueReference(). At this point, a.getInt() is guaranteed to have
// been evaluated.
{
A a;
passByRvalueReference(a.getInt(), std::move(a));
}
// [snip]
} If we assumed that the move already happens in
These are two separate scenarios:
As explained above, this would cause false positives for |
(Sorry for the late reply, I only saw this issue today.) |
"moving from the same pointer twice has defined behavior that programmers may intentionally exploit." But why would you move from same pointer twice ? You already know as a developer that after first move this pointer is null, so just instead of referencing pointer just put nullptr into code. Or re-initialize pointer. Using object after passing it as an argument to std::move, is just a potential bug. It may work in many cases, but it's a code-smell. I can understand why clang-analyser may want to focus only on clean bugs, but clang-tidy should just raise warnings for all those potential issues. |
The specific case of moving from the same pointer twice is indeed relatively rare, though I've seen people do this in loops, where the intent is to pass the non-null pointer on the first iteration of the loop, and a null pointer on subsequent iterations. I think I've also seen cases where people conditionally move from a smart pointer, and then later move that same pointer (which now may or may not be null) to some other place. But other uses of moved-from smart pointers are pretty common and definitely require special treatment to avoid false positives, specifically If you would like to forbid use-after-moves of smart pointers entirely on your codebase, then one option could be to introduce a different mode for the check (though
clang-tidy can afford a higher false positive rate than other tools, but even here, false positives have a cost. |
"As explained above, this would cause false positives for std::unique_ptr (and the other standard smart pointers), where moving from the same pointer twice has defined behavior that programmers may intentionally exploit." But that is a bugprone code. After all check name is "bugprone-use-after-move", you do not know if developer re-used variable on purpose or by mistake. And if moved then they could use null instead of referencing variable again. Check shouldn't treat smart pointers in different way to other objects. Problem is same, and it doesn't matter whatever object is left in usable state or not, it's still use after move. "But other uses of moved-from smart pointers are pretty common and definitely require special treatment to avoid false positives, specifically operator bool and == nullptr, which can be used to check whether a smart pointer has been (conditionally) moved from." "If you would like to forbid use-after-moves of smart pointers entirely on your codebase, then one option could be to introduce a different mode for the check (though CheckOptions) in which it treats smart pointers like all other types." I got over 30 000 usages of std::move in code, most of them on smart pointers. And now after applying fix from #94869, 3 new real bugs popup (luckily not in critical code), there were no false-positives related to std::move (had few related to std::forward but thats because i took llvm 19 version of check and applied to llvm 18). I can't live with 3 real bugs because for check nullptr smart pointers are fine. |
Before responding to individual points, let me stress that I understand your concerns, I'm open to considering changing the behavior of the check for smart pointers, and I understand that doing so has enabled you to find real bugs. I just want to make sure that this change isn't a change for the worse in other codebases / for other users.
There is an important difference though: For smart pointers, the behavior is deterministic; for other types, it is essentially non-deterministic. The standard explicitly defines the behavior that smart pointers must have when moved from, so it's reasonable to expect that some programmers will take advantage of this behavior. It can be argued that doing so is bad style (many other things that are explicitly defined in C++ are widely considered bad style), but use-after-move on other types is not just bad style but essentially non-determinism.
Understand, I just feel as if other users of the check might see this differently. It would be great to get some more input on this from other users of this check (though I'm not sure what the best way of doing that would be).
Can you share source code for these bugs? I'd be really interested to see what actual source code looks like that gets this wrong, because that usually gives a much more realistic "feel" than discussing synthetic examples. If this is a closed-source codebase, is there any way you can "anonymize" the code samples? I've tried searching for examples where code intentionally exploits the moved-from behavior of smart pointers, and does so in a way that seems defensible, but I haven't been able to come up with anything (though it's pretty hard to search for this kind of thing). I do feel as if I saw legitimate examples many years ago when I first developed the check, and that I added the special treatment of smart pointers because of this, but I don't recall any details. tl;dr: I would love to see some real-world examples, and let's discuss further. |
"Can you share source code for these bugs?" 2 of 3 issues were like this (but without typedefs):
If you swap to SS, then it's detected. We do not have code that "intentionally exploits the moved-from behavior ". So after getting rid of custom smart pointer handling all new issues that popup were real. Thing is that we got check that adds std::move where it's appliable and in balance this check is used to make sure that there are no bugs. |
(Sorry for the late reply -- this dropped off my radar.)
Thanks -- interesting! I see how this pattern is particularly subtle because the parameter and the member variable are named the same, and it's easy to forget what the
Got it. It makes sense that you'd like to catch bugs like the above. I'm still a bit hesitant about flipping the behavior without feedback from other users. I'm not sure what the best way would be to solicit feedback -- maybe submit a short post to the clang-tidy forum describing the proposed change in behavior (with a link to this bug) and asking for feedback? |
I've been trying to apply the clang-tidy checker to my codebase, and have a problem related to the discussion happening here. My codebase has an intrusive reference counted smart pointer class which provides strong guarantees on the state after being moved from (e.g. set to nullptr). My codebase is >20 years old and worked on continuously by >50 engineers since the beginning. We also had move-semantics since before C++11 via a custom standard-library implementation that had functions specifically for moving the guts of things into another object, but have almost entirely switched to C++11 rvalue-references. As such, we have a very large amount of code that assumes it's perfectly fine to move a smart pointer twice, or to compare two pointers regardless of whether one of them was moved, or compare a moved-from pointer to nullptr. Moving the smart pointer twice happens quite often in heavily templated code, where it doesn't particularly matter to the programmer that it's happening as they are just calling utility functions which happen to move (and it's well defined what happens on the second occurrence). And comparing the moved-from object to a particular value (nullptr, another ptr, whatever) happens thousands of times in our unit test suite. To the point where it's not worth it for me to try to track down every place it's happening and i'm more interested in turning off the use-after-move check entirely. There was also this discussion here: #46 I can't think of anything that could be done to improve the double-move situation, but i'm convinced that clang needs to support some kind of attribute like https://clang.llvm.org/docs/AttributeReference.html#reinitializes that can be used to tag member functions as safe to be called on a moved-from object. Without an attribute like, i don't know, I have tons of code where calling the function in question doesn't make the object "no longer moved from", but where calling the function is harmless. Instead of building a list of "safe" member function names, i think an attribute would be the safer approach. |
Example:
No issue from bugprone-use-after-move or from clang-analyzer-cplusplus.Move.
Example 2:
Detected by clang-analyzer-cplusplus.Move, but not by bugprone-use-after-move
The text was updated successfully, but these errors were encountered: