This is an archive of the discontinued LLVM Phabricator instance.

[clang-tidy] new "throw-by-value-catch-by-reference" check for clang-tidy
ClosedPublic

Authored by randomcppprogrammer on Jul 18 2015, 8:44 AM.

Details

Summary

This patch contains an implementation to check whether exceptions where thrown by value and caught by const reference. This is a guideline mentioned in different places, e.g. "C++ Coding Standards" by H. Sutter.

Patch by Tobias Langner

Diff Detail

Event Timeline

randomcppprogrammer retitled this revision from to [clang-tidy] new "throw-by-value-catch-by-reference" check for clang-tidy.
randomcppprogrammer updated this object.

Updated check according to comments given by Aaron Ballman. Most notable change: added optional check for throwing anonmyous temporaries.

aaron.ballman edited edge metadata.Sep 8 2015, 6:53 AM

This is looking great! Some mostly minor feedback below. The only things I noticed that may require further consideration are throwing function parameters and throwing exception handler variables -- I don't think those scenarios should emit a diagnostic. The former is a bit chatty because of error handling functions, and the latter is correct regardless of the type being thrown (though is a bit silly because the user should really just use throw; instead of rethrowing the variable).

General nitpick: please make sure all comments in the code and tests have proper capitalization, punctuation, etc.

clang-tidy/misc/ThrowByValueCatchByReferenceCheck.cpp
24

Can we just use a bool instead of the string "true"? The constructor for AssertSideEffectCheck does this, for instance.

32

I'm not certain whether this is feasible or not, but if you can check the options by this point, you could replace a lot of custom AST logic from check() by registering a different matcher if care about anonymous temporaries. For instance:

if (checkAnonymousTemporaries)
  Finder->addMatcher(throwExpr(unless(anyOf(hasDescendant(anyOf(declRefExpr(to(parmVarDecl())), declRefExpr(to(varDecl(isExceptionVar()))), constructExpr(hasDescendant(materializeTemporaryExpr())), expr(hasType(hasCanonicalType(builtinType()))))), unless(has(expr()))))), this);
else
  Finder->addMatcher(throwExpr(), this);

(Leaving the bindings out, but you get the idea).

44

No need for the != nullptr.

May want to consider splitting the throw and catch logic into helper methods to reduce indenting. e.g.,

if (throwExpr)
  doThrowStuff(throwExpr);

if (catchExpr)
  doCatchStuff(catchExpr);
45

I think we want throwExpr->getSubExpr()->IgnoreParenImpCasts(). This looks through *all* implicit casts (and paren expressions, etc), so you can get rid of the if (isa<ImplicitCastExpr>) from below and just look at isa<StringLiteral>. This will then allow code like the following to be properly checked: throw ("I am not a kind person");

56

How about: "throw expression throws a pointer value; should throw a non-pointer value instead"?

64

No need to state that this is an algorithm. The reader can likely guess as much. ;-)

75

I think we want IgnoreParenImpCasts() here as well.

99

IgnoreParenImpCasts(), again?

103

I think the correct way to do this is check to see whether the constructor is passed a MaterializeTemporaryExpr. and if so, we don't want to emit the diagnostic (otherwise we do).

Also, this code does not look to see whether there is a DeclRefExpr that refers to a parameter variable declaration. In my testing, this accounted for a sizable number of false positives because of code like:

void handle_error(const someObj &o) {
  throw o; // Shouldn't diagnose, this isn't dangerous
}
109

How about: "throw expression should throw an anonymous temporary value instead"?

113

No need for the != nullptr.

120

Should use getCanonicalType() instead of the internal version.

125

How about: "catch handler catches a pointer value; should throw a non-pointer value and catch by reference instead"?

May also want to consider combining with the same diagnostic below (or, at the very least, only have the diagnostic text written once and referred to from both places).

129

"do not advice" -> "do not diagnose"

test/clang-tidy/misc-throw-by-value-catch-by-reference.cpp
4

Should remove this.

20

Don't worry about 80-col limits for diagnostic messages -- keep the entire diagnostic on one line (it makes it easier to read the tests).

45

You can add your own move definition to the file:

template <class T> struct remove_reference      {typedef T type;};
template <class T> struct remove_reference<T&>  {typedef T type;};
template <class T> struct remove_reference<T&&> {typedef T type;};

template <typename T>
typename remove_reference<T>::type&& move(T&& arg) {
  return static_cast<typename remove_reference<T>::type&&>(arg);
}
54

I don't think we should diagnose on this case; I found it caused a lot of false positives for code bases that wrap throws in an error handling function.

139

I'd also like to see tests for:

typedef logic_error& fine;
try {
} catch (int i) { // ok
  throw i; // ok
} catch (fine e) { // ok
  throw e; // ok
} catch (logic_error *e) { // diagnose for catching a pointer
  throw e; // ok, despite throwing a pointer
} catch(...) { // ok
  throw; // ok
}
jbcoe resigned from this revision.Oct 5 2015, 6:20 AM
jbcoe removed a reviewer: jbcoe.
jbcoe added a subscriber: jbcoe.
randomcppprogrammer edited edge metadata.
randomcppprogrammer marked 17 inline comments as done.

reworked code to include the changes suggested by Aaron Ballman

main changes

  • will not diagnose on throwing catch variables by value/pointer
  • will not diagnose on throwing function parameters (if you are using special handler functions for throwing)

This patch is looking much closer! Thank you for continuing to work on it.

I found several mechanical changes that need to be made, like style, comments, formatting, isa<> followed by cast<>, etc. There is one logic issue regarding materialized temporaries that I think still needs to be addressed, but otherwise, this patch is getting close!

clang-tidy/misc/ThrowByValueCatchByReferenceCheck.cpp
47

Judging by the function name, I think the code should actually be:

return isa<ParmVarDecl>(declRefExpr->getDecl());
61

Instead of isa<> followed by cast<>, the more idiomatic approach is:

if (const auto *VD = dyn_cast<VarDecl>(declRefExpr->getDecl()))
  return VD->isExceptionVariable();
67

Should run clang-format over this; the brace should go with the closing paren.

79

No need to call getTypePtr(); QualType objects are thin wrappers around a Type *. So you can do qualType->isPointerType(), for instance.

87

Space between the comment and the start of the sentence; also, comments should be full sentences (with capitalization and punctuation). Applies here and elsewhere in the patch.

89

Please use dyn_cast<> instead of isa<> followed by cast<>. Applies here and elsewhere in the patch.

94

non-pointer, please

112

This should be:

if (const auto *variableReference = dyn_cast<DeclRefExpr>(currentSubExpr)) {
} else if (const auto *constructorCall = dyn_cast<CXXConstructExpr>(currentSubExpr)) {
}
130

I think that what we need to model here is: constructExpr(hasDescendant(materializeTemporaryExpr())) and expr(hasType(hasCanonicalType(builtinType())))

If the throw expression materializes a temporary, or it throws a builtin type, we are good. (Note, the checking for function parameter or catch variable is already done properly.)

157

No need to call getTypePtr().

160

Same here.

Also, please do not use getCanonicalTypeInternal(), you should use getCanonicalType() instead.

163

A cleaner way to do this would be:

if (const auto *PT = getCanonicalType(caughtType)->getAs<PointerType>()) {
  if (!PT->getPointeeType()->isAnyCharacterType())
    diag(...);
}

This also corrects direct comparison against a Boolean on the next line.
clang-tidy/misc/ThrowByValueCatchByReferenceCheck.h
44

Should be Check instead of check per style guidelines.

randomcppprogrammer marked 11 inline comments as done.

Incorporated feedback from Aaron Ballmann

main changes:

  • replaced isa followed by cast with dyn_cast
  • reworked comments
  • fixed typo in diagnosis message

On Wed, Oct 7, 2015 at 5:08 PM, Tobias Langner <randomcppprogrammer@gmail.com> wrote:

Hi,

I incorporated most of your changes. There are 2 open issues

Thank you for your work on this! There are a few minor nitpicks still, but is definitely moving forward nicely.

  • on one location I could not follow your advice, the compiler refused to

compile the code. I chose a different approach and hope you like it.

  • the second thing is this MaterializeTemporary advice that you gave. I

don’t fully understand it (possibly due to a lack of understanding the AST
and a lack of documentation of the proposed methods). Could you please flesh
out your idea and why you think it is necessary? Or at least give an example
where the current code does not work correctly.

Consider code like:

struct S {};

S& g();
S h();

void f() {
  throw g(); // Should diagnose, does not currently
  throw h(); // Should not diagnose, does not currently
}

"throw g();" isn't throwing a temporary because it's throwing from an lvalue reference object (so the user may have a catch clause expecting the caught object to be the same lvalue reference as what g() returns, except that's not actually the case). If you instead check to see whether the throw expression requires a temporary to be materialized, you'll find that g() will diagnose, while h() still will not.

Does that help?

clang-tidy/misc/ThrowByValueCatchByReferenceCheck.cpp
56

How about:

if (const auto *VD = dyn_cast<VarDecl>(declRefExpr->getDecl()))
  return VD->isExceptionVariable();
return false;
73

Sentence capitalization and punctuation in the comment.

78

Sentence capitalization and punctuation in the comment.

95

Sentence capitalization and punctuation in the comment.

122

Sentence capitalization and punctuation in the comment.

144

I think this would be better as:

if (const auto *PT = caughtType.getCanonicalType()->getAs<PointerType>()) {
  if (...)
    diag(...);
} else if (!caughtType->isReferenceType() && !caughtType.isTrivialType(context))
  diag(...);

As-written, it currently checks whether the caught type is a pointer, but not whether the canonical caught type is a pointer.

test/clang-tidy/misc-throw-by-value-catch-by-reference.cpp
46

This comment is outdated.

randomcppprogrammer marked 7 inline comments as done.

new:

  • diagnosis on throwing lvalues returned by function calls such as

struct S;
S& function();

void foo() {

throw function();

}

  • updated tests for this case
  • updated some comments
aaron.ballman accepted this revision.Oct 9 2015, 1:06 PM
aaron.ballman edited edge metadata.

LGTM! Please run your patch through clang-format before committing (just to fix the last bits of style and whitespace issues; no additional review required).

Thank you for your hard work on this! As a cleanup once this goes in: we should add some user-facing documentation to docs\clang-tidy\checks for it. We can even take what's in ThrowByValueCatchByReferenceCheck.h and use that (then link to it from the header file instead of duplicating the words).

This revision is now accepted and ready to land.Oct 9 2015, 1:06 PM
randomcppprogrammer edited edge metadata.

fixed formatting

aaron.ballman closed this revision.Oct 9 2015, 1:44 PM

I have commit in r249899. Thanks again!