15

I'm trying to write exception safe code. I find that using C++11's noexcept specifier makes this goal a whole lot more achievable.

The general idea, of course, is that a function should be marked as 'noexcept' if, and only if all the functions that it calls are also marked as 'noexcept'.

The problem is that in a large code base, where patches from different people are often merged together, it is hard to ensure that this consistency is maintained.

So I would like to be able to run a static analysis that could list all places where a function that is marked 'nothrow' calls a function that is not marked as 'nothrow'.

As far as I can see in the man-page, GCC cannot help me here. Are there any stand-alone tools that can help me? Or maybe some other compilers?

Kristian Spangsege
  • 2,903
  • 1
  • 20
  • 43
  • 8
    This is always a nice idea, but it may fail in practice as the complexity of your no-throw functions increases. An example I saw elsewhere: `double safe_sqrt(double x) { if (x < 0) throw "no"; return sqrt(x); } double abs_sqrt(double x) noexcept { return safe_sqrt(abs(x)); }` The noexcept function calls a throwing function, but this is safe because the throwing path is impossible to reach. However, static analysis is no longer as simple as "do I call noexcept functions". Still doable, sure, but a lot harder. And there are workarounds, but I doubt they're simple in every case (this one is). – GManNickG Feb 01 '13 at 18:02
  • Yes, that is a problem, however, in such cases you would either accept a "false positive" from the analyser, or simply do not mark abs_sqrt as 'noexcept'. I still feel that such a tool would provide great value. – Kristian Spangsege Feb 01 '13 at 18:09
  • 1
    btw you might wanna read this: http://akrzemi1.wordpress.com/2011/06/10/using-noexcept/ (somebody posted it to my noexcept Q) – NoSenseEtAl Feb 01 '13 at 18:11
  • 2
    Take a look at clang... it might not implement it already, but it is simple to extend the analyzer – David Rodríguez - dribeas Feb 01 '13 at 18:12
  • @NoSenseEtAl: Ah, that's where I saw it. :) – GManNickG Feb 01 '13 at 19:10
  • @KristianSpangsege: For sure (I would accept the false positive), just be wary of putting too much time into it if the gains start to diminish. – GManNickG Feb 01 '13 at 19:11
  • 2
    @NoSenseEtAl: According to andrzej's blog "Static checking of no-throw guarantee, with an improved tool for locally disabling the check." is likely to be part of the next version of C++. Nice! – Kristian Spangsege Feb 01 '13 at 20:52
  • It is impossible. A diagnostic program could follow all call-paths that appear to be reachable, but the logic of the input-program might make some of those paths unreachable. In other words, the diagnostic program invariably would be subject to false positives. Grok "the Halting Problem." https://en.wikipedia.org/wiki/Halting_problem – Jive Dadson Nov 20 '17 at 02:34
  • @JiveDadson Indeed, so one would need a way to declare towards such a tool that some blocks of code never throw. Ideally something like `noexcept { ... }`. – Kristian Spangsege Nov 27 '17 at 12:38
  • Personally, I'd like to see the Java approach to exception checking in C++, where runtime errors like `bad_alloc`s are not checked, but all logic errors are. Whether or not it's actually possible for an exception to be thrown is besides the point: if the function is marked `noexcept(false)`, you must mark the caller function as `noexcept(false)` or use a try-catch. – Chris Watts Dec 18 '18 at 14:26

2 Answers2

6

It certainly seems possible if you avoid pointer to functions (and deem them unsafe) by using the ABT of the program. Clang's AST is (despite its name) such an ABT: you will see both declarations and definitions of the functions. By doing your work one definition at a time, you will already have a good baseline.

On the other hand, I wonder whether this is practical. See, the problem is that any function performing a memory allocation is (voluntarily) marked as potentially throwing (because new never returns null, but throws bad_alloc instead). Therefore, your noexcept will be limited to a handful of functions in most cases.

And of course there are all the dynamic conditions like @GManNickG exposed, for example:

void foo(boost::optional<T> const t&) {
    if (not t) { return; }

    t->bar();
}

Even if T::bar is noexcept, dereferencing an optional<T> may throw (if there is nothing). Of course, this ignores the fact that we already ruled this out (here).

Without having clear conditions on when a function might throw, static analysis might prove... useless. The language idioms are designed with exceptions in mind.


Note: as a digression, the optional class could be rewritten so as not to exposed dereferencing, and thus be noexcept (if the callbacks are):

template <typename T>
class maybe {
public:

    template <typename OnNone, typename OnJust>
    void act(OnNone&& n, OnJust&& j) noexcept(noexcept(n()) and 
                                              noexcept(j(std::declval<T&>())))
    {
        if (not _assigned) { n(); return; }
        j(*reinterpret_cast<T*>(&_storage));
    }

private:
    std::aligned_storage<sizeof(T), alignof(T)>::type _storage;
    bool _assigned;
};

// No idea if this way of expressing the noexcept dependency is actually correct.
Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • Well, dynamic allocations may fail, that is just a fact of life. For this reason (and others) I try hard to avoid dynamic allocations. In the code that I work on, dynamic allocation is the main reason that all functions aren't 'nothrow'. – Kristian Spangsege Feb 01 '13 at 18:52
  • @KristianSpangsege: Unfortunately, it *may* fail but at least on Linux it won't because of overcommit and you'll get a segfault instead. So you pay the `noexcept` price for it, but your program will crash before a `bad_alloc` is thrown: it's a lose/lose situation. – Matthieu M. Feb 01 '13 at 18:59
  • I'm pretty sure dynamic allocation can fail on Linux too if virtual memory is limited specifically for your process with `ulimit`. – Kristian Spangsege Feb 01 '13 at 20:15
  • 1
    On OSX, however, there is no way to limit virtual memory for a process, so on this platform it really may be impossible for dynamic allocation to fail. – Kristian Spangsege Feb 01 '13 at 20:20
  • @KristianSpangsege: maybe... I certainly don't know the ins and outs; however `bad_alloc` still is a broken promise, because a promise should be held at all times! And this fancy promise, unfortunately, make `noexcept` analysis pretty much useless. Unless it's possible to overload the global `new` and allocators with `noexcept` versions maybe, but even then I am not sure the Standard Library implementation will correctly propagate the attribute. – Matthieu M. Feb 02 '13 at 13:59
  • @JiveDadson: The Halting Problem is a non-issue. We are not trying to prove that a sub-part of the program ends, just that it doesn't throw an exception. It's not our problem if it gets stuck in an infinite loop, pauses until 2036, ... it just should not throw. And that's well within our capabilities to prove, provided that the user assists by annotating functions and writing code in a way which avoids throwing functions (such as the `maybe` rewrite). – Matthieu M. Nov 20 '17 at 07:44
  • @Matthieu - The same diagonalization logic that proves the halting problem to be unsolvable can be applied to a program that purports to solve the "throwing problem", can it not? Admittedly, I did not think about it too hard. Whether or not the "TP" is solvable if the "user assists" would of course depend on the user and the assistance. (I would probably screw it up.) – Jive Dadson Nov 20 '17 at 08:12
  • @JiveDadson: For this kind of analysis, there are two situations (1) DAG and (2) cycles. The DAG parts are easily handled, it's only the cycles which require some care. I think maybe an evil user could attempt to throw the algorithm in a loop by using some overload resolution which depends on whether a function throws or not... not quite sure. I am not concerned though: the compiler could, in this case, simply emit a diagnostic and abort compilation. And then it's on the user to fix this. – Matthieu M. Nov 20 '17 at 08:52
  • 1
    @Mattieu - Forget loops. My point is only that it is theoretically impossible for a single program to decide if arbitrary programs will ever throw, given any input. EOT – Jive Dadson Nov 20 '17 at 09:06
  • 1
    @JiveDadson: Sure, but we are not talking about *arbitrary* programs; there is no *dynamic* condition handling here! We are not asking the compiler to prove that none of the functions called with throw, but instead to prove that all functions called are annotated with `noexcept`. That's vastly simpler! It's also vastly more restrictive, of course, because you cannot call a potentially throwing function even if you attempt to dynamically assert that in your case it will not throw => but this very simplification is what I think could allow the proof. – Matthieu M. Nov 20 '17 at 09:43
  • @Matthieu M. Whether Linux over-commits always, heuristically or never is configurable. You can't rely on "always overcommits". – Jesper Juhl Oct 19 '18 at 16:09
1

The clang-tidy check bugprone-exception-escape attempts to find functions which may throw an exception directly or indirectly, when they should not. This includes checking functions marked with throw() or noexcept.

https://clang.llvm.org/extra/clang-tidy/checks/bugprone/exception-escape.html

Alexander Kondratskiy
  • 4,156
  • 2
  • 30
  • 51
Bowie Owens
  • 2,798
  • 23
  • 20