Returning class or struct by value with const qualifier should be avoided since it prevents move semantics.
clang-tidy has readability-const-return-type that catches this case, but it also impacts move semantics, not just readability.
Differential D125402
[clang][diag] warn if function returns class type by-const-value nlee on May 11 2022, 10:15 AM. Authored by
Details
Returning class or struct by value with const qualifier should be avoided since it prevents move semantics. clang-tidy has readability-const-return-type that catches this case, but it also impacts move semantics, not just readability.
Diff Detail Event TimelineComment Actions Thank you for working on this new diagnostic! We don't typically add new diagnostics that are off by default unless they're for pedantic diagnostics or are reasonably expected to be enabled by default in the future. This is because we've had evidence that suggests such diagnostics aren't enabled often enough to warrant the maintenance cost for them. I'm not convinced this diagnostic can be enabled by default. Yes, it prevents move semantics, but that's not typically an issue with the correctness of the code. IIRC, we decided to put this diagnostic into clang-tidy because we thought it would be too chatty in practice, especially on older code bases. Perhaps there are ways we can improve the diagnostic behavior to allow it to be enabled by default though. One possibility would be to look at the return type to see if there's a user-provided move constructor (either = default or explicitly written) and only diagnose if one is found. But I think we'd want some evidence that this actually reduces the false positive rate in practice, which means trying to compile some large C++ projects (of various ages/code quality) to see. Would you be willing to run your patch against some large C++ projects to see what kind of true and false positives it generates? From there, we can decide whether we need additional heuristics or not.
Comment Actions How about adding CXXRecordDecl::hasMoveConstructor() to ensure the type is movable? I ran a test with OpenCV(c++11), and it returned some useful warnings: In file included from /Users/nlee/Documents/opencv/modules/core/include/opencv2/core/base.hpp:662: /Users/nlee/Documents/opencv/modules/core/include/opencv2/core/check.hpp:16:12: warning: returning by const value prevents move semantics [-Wreturn-by-const-value] CV_EXPORTS const String typeToString(int type); ^~~~~~ /Users/nlee/Documents/opencv/modules/core/include/opencv2/core/check.hpp:26:12: warning: returning by const value prevents move semantics [-Wreturn-by-const-value] CV_EXPORTS const cv::String typeToString_(int type); ^~~~~~ @aaron.ballman Can you suggest some other projects to test? I'm thinking of these: protobuf, pytorch
@erichkeane Does adding CXXRecordDecl::hasMoveConstructor() suffices to ensure the type is movable? I left out unions because they may alert false positives. An example of such a case is: union U { int i; std::string s; }; const U f() { return U{239}; }
Comment Actions Thank you for your reviews, and sorry for the mess in my last comment. I'm new to clang/Phabricator and trying to get used to it :)
Comment Actions I don't think that will be quite correct -- IIRC, that would still return true if the move constructor was deleted. hasSimpleMoveConstructor() and hasSimpleMoveAssignment() might be a better approach.
Those are good; I'd also run it over LLVM itself. Boost would also be a good thing to test against.
No problem, Phabricator takes some getting used to and the Clang code base is pretty large. :-)
Comment Actions The 'Simple' version might not be quite right... That is implemented as: bool hasSimpleMoveConstructor() const { return !hasUserDeclaredMoveConstructor() && hasMoveConstructor() && !data().DefaultedMoveConstructorIsDeleted; } So this would still warn about user-defined move constructors. HOWEVER, I might suggest hasMoveConstructor() && !defaultedMoveConstructorIsDeleted() for the ctor test. There is similar storage for the 'DefaultedMoveAssignmentIsDeleted`, but it isn't exposed, so you might need to add a function to expose it in DeclCXX.h. Comment Actions Good catch!
I think that might still not be correct because there could be a user-provided move constructor that's explicitly deleted. (So it has a move constructor, but the defaulted one is not deleted because it's user provided.) Comment Actions I don't think that is possible? I think 'defaultedMoveConstructor' includes the user typing StructName(StructName&&) = delete;. Note that out of line deleted implementations are prohibited: https://godbolt.org/z/MTK4jGa57 So we don't have to worry about that. Comment Actions I might be wrong about this above: /// constructor, but overload resolution failed so we deleted it." So perhaps more work to figure out non-deleted move ctor, it might require a lookup. So if we do that, we need a bunch of tests to validate all these conditions I believe (manually deleted, inherited deleted, member-cauesd-deleted, and 'defaulted' versions of the last 2, user provided but works, etc). Comment Actions After some investigation, I think it is not always explicit that removing const makes any improvements when we are "constructing." For example, in this case, the two codes which differ in the return type of function f (S f() vs. const S f()) produce the same assembly output. I assume this is due to various copy elisions stepping in. However, when we are "assigning," it makes a difference. In this case, the existence of const in the return type of f decides whether the assignment invokes a move or not. So I'm suggesting a different policy. Warn if:
The following is a part of diagnosed warnings when applied to llvm: /Users/namgoolee/Documents/llvm-project/llvm/include/llvm/Option/Option.h:103:9: warning: const qualifying the return value prevents move semantics [-Wpessimizing-return-by-const] const Option getGroup() const { ^ /Users/namgoolee/Documents/llvm-project/llvm/lib/Option/ArgList.cpp:38:10: note: copy assignment is invoked here even if move assignment is supported for type 'llvm::opt::Option' O = O.getGroup()) { ^ -- /Users/namgoolee/Documents/llvm-project/clang/include/clang/Lex/MacroInfo.h:421:9: warning: const qualifying the return value prevents move semantics [-Wpessimizing-return-by-const] const DefInfo findDirectiveAtLoc(SourceLocation L, ^ /Users/namgoolee/Documents/llvm-project/clang/include/clang/Lex/Preprocessor.h:1167:10: note: copy assignment is invoked here even if move assignment is supported for type 'clang::MacroDirective::DefInfo' DI = MD->findDirectiveAtLoc(Loc, getSourceManager()); ^
Comment Actions Warn if:
Removed DefaultIgnore flag and make check-clang passes. Comment Actions This has been sitting in the queue for a while, sorry about that.
|
Given that this is a DefaultIgnore, I'm still not quite sure this belongs in Clang rather than continuing to be covered by clang-tidy.
If you leave it on by default, how many Clang tests break as a result? Do the new warnings all look like true positives? (Basically, I'm trying to see how far away we are from being able to enable this diagnostic by default.)