noexcept destructors

The goal of this post is to show one — fairly small — backwards incompatibility in C++11. It shows how noexcept exception specifications are implicitly generated for your destructors. In short, the following program used to run successfully in C++03 (under some definition of “success”):

struct S
{
  ~S() { throw runtime_error(""); } // bad, but acceptable
};

int main()
{
  try { S s; }
  catch (...) {
    cerr << "exception occurred";
  } 
  cout << "success";
}

In C++11, the same program will trigger the call to std::terminate.

Who does deliberately throw from destructors anyway?

You might think that this is not really an observable problem, because destructors are not supposed to throw anyway. Indeed we all have been taught that destructors had better not throw. Nonetheless, the language still allows this; and while generally dangerous, people are still allowed to do so, and explore whether there are cases where throwing destructors are useful.

Apart from resource disposal some programmers tend to use destructors (quoting Mortoray) for completion of business logic process. For instance, they use destructor to commit DB transactions:

void storeCount(int count)
{
  transaction tr(DB);
  tr.execute("insert into counts values(%1)", count);
} // implicit commit

One fairly popular library that uses this technique is SOCI. Even though throwing destructors are generally considered a bad and dangerous practice, this library have grown popular for years. I guess, it works on the assumption that noöne attempts to store things in DB during stack unwinding. For another inventive use of throwing destructors see this post by Stefanus Du Toit.

In this post I do not intend to argue whether it is a bad practice or not to throw from destructors, but focus on what happens when you do. But I really do not encourage you to throw from destructors.

Implicit exception specification

As one might expect, for nearly any function (except destructors and deallocation functions), unless you specify an exception specification explicitly, the function is allowed to emit any exception. The case is different for destructors: the compiler will deduce the exception specification, and it may deduce it based on different rules than you expect.

Even if you explicitly declare your destructor without any exception specification, the compiler will add one for you: the same as it would add if it had to implicitly generate the destructor. If you do not like the exception specification generated by the compiler, you have to explicitly specify it.

The implicitly generated destructor calls destructors of non-static member sub-objects and base-class sub-objects. The implicitly generated exception specification is based on exception specifications of the destructors being called. If some exception is allowed by the sub-object’s destructor, it will also be allowed by our destructor. If some exception is not allowed by any sub-objects’ destructor exception specification, it will not be allowed by our destructor’s exception specification either.

struct A {}; // 'implicit' destructor implicitly noexcept(true)

struct B 
{
  int i;
  ~B() {} // 'explicit' destructor implicitly noexcept(true)
};

struct R : A
{
  B b;
  ~R() { // still implicitly noexcept(true) !
    throw runtime_error("");
  }
};

Note what happens here: A’s destructor is implicitly declared and defined: it has no sub-objects, so it does nothing: compiler adds exception specification noexcept(true). Class B does have a sub-object of type int; it also has a user-defined destructor. Because int’s destructor does not throw any exception, so noexcept(true) specification is implicitly added, even though we cannot see this specification in the code.

The case of R’s destructor is the most surprising. In the body of the inline destructor we unconditionally throw an exception; it is obvious that the destructor will throw. But the logic of the compiler is different. It tries to determine what the destructor would be if it had been implicitly defined: in that case it would only call its sub-objects’ destructors and do nothing inside the body: no throwing of exceptions. Neither of its sub-objects is declared to throw from destructors, so our destructor — if it were implicitly defined — would be noexcept(true). So the compiler uses the exception specification from the hypothetical implicitly-generated definition even though we do provide our own, and even though we throw inside the body. The contents of the body is irrelevant when deducing the exception specification.

The only thing that can change the implicit noexcept(true) specifications are explicit noexcept(false) specifications. In case you need R::~R to throw, the simplest way to allow it is to declare it as noexcept(false). There is also another way. You can declare either A::~A or B::~B as noexcept(false):

struct A {}; // implicitly noexcept(true)

struct B 
{
  int i;
  ~B() noexcept(false) {}
};

struct R : A
{
  B b;
  ~R() { // now implicitly noexcept(false)
    throw runtime_error("");
  }
};

Now the hypothetical implicitly defined destructor of R would need to call B::~B, which is noexcept(false), therefore R::~R would also have to be noexcept(false).

You can see that the specifications are ‘transitive’. This allows an intuitive composition of classes: if your sub-object has a throwing destructor, your destructor is also throwing.

Some technical details

I have simplified the above story a bit. I was only talking about noexcept exception specifications. C++11 still supports the old ‘dynamic’ exception specifications:

struct B 
{
  ~B() throw(Exc1, Exc2, system_error);
};

They are deprecated, but still mandated by the Standard. Deducing exception specifications in such case means determining the union of all possible exceptions declared to be thrown from all possible destructors.

In case some of your sub-object’s destructors have noexcept specifications and some other have ‘dynamic’ exception specifications, the situation is even more complicated. And in fact, the C++ Standard does not specify precisely what happens in the following case:

struct A 
{
  ~A() throw(); // 'dynamic' no-throw
};

struct B 
{
  ~B() noexcept; // 'static' no-throw
};

struct R : A
{
  B b;
  ~R() /* no-throw */ ;
};

R::~R should certainly be deduced as non-throwing. But should it be a dynamic or a static version? C++11 leaves it unspecified, even though the difference may be observable in the program: violating dynamic exception specifications follows different rules (calling std::unexpected, throwing std::bad_exception).

In case you wonder where exactly this is defined in the Standard, the two references are §12.4 ¶3 and §15.4 ¶14.

Note also, that another exceptional rule for implicit exception specifications is made for deallocation functions. The rules are simpler, though: unless you explicitly state otherwise, any deallocation function is implicitly declared as noexcept(true). The reference to the Standard: § 15.4 ¶ 15.

This entry was posted in programming and tagged , , , . Bookmark the permalink.

23 Responses to noexcept destructors

  1. mgaunard says:

    Many objects have destructors that can fail, it’s not just SOCI. Consider buffered I/O like std::fstream for example.

    • “Many” is a very subjective statement. I guess different code bases have different factors. The code I used to see used to have most of the destructors implicitly generated.
      The fact that destructor fails (doesn’t do what it has been requested) doesn’t mean that it throws (reports the failure). This is the case for std::fstream in C++11 (it has changed from C++03), and more precisely, with std::basic_filebuf. 27.9.1.3 para 5 requires of the destructor:

      “Destroys an object of class basic_filebuf<charT,traits>. Calls close(). If an exception occurs during the destruction of the object, including the call to close(), the exception is caught but not rethrown”

      This is also a typical advice given to programmers: if something fails during the destruction, swallow the exception. SOCI departs from this advice.

      • Many programmers often don’t realize that some things can fail, and that they need to ensure that the program state (which might include files on the disk or remote connections) stays valid and consistent regardless of failures.
        Masking the failure is not a solution. This just leads to crappy software.

        This design is unfortunate because it means that anyone that uses std::fstream and cares about handling failure properly must call close explicitly to be able to handle it properly.

        • I agree with the observation that swallowing exceptions in destructor effectively means concealing the information about failure.

          Fortunately, the language allows you to make the choice whether you want to throw from destructors or not. The purpose of this post is to indicate that if you chose to throw, you must also remember to annotate your destructor with noexcept(false).

          On the other hand, (continuing with your example of std::fstream) I am not entirely convinced that you practically loose anything by swallowing the exception from destructor. You created a file object. This alone means “compiler, close it yourself, I do not want to be bothered”. If it threw, how do you propose to handle it (apart from displaying a log message)? The object is gone, you do not have the handle to the file; nor the file name (most probably). What do you do?

          (I am not really trying to convince you. I am trying to make up my own mind. I seam to be torn.)

  2. Nice informative post. Thanks Andrzej

  3. ph says:

    nonthrowing destructors is the bullet that rips a gaping hole into RAII: if releasing th resource can (or should) throw, RAII becomes a burden.

    We could tell RAII to stick to resources where release is always successful, but that leaves a swarm of shotgun pellets in the body of exceptions, as nontrivially released resources lead to painfully substandard syntax and API’s.

    • I sympathize with your view. Therefore I am not entirely convinced that “never throw from your destructors” is the ultimately correct advice.

      On the other hand, one does not report failures for reporting’s sake or for elegance’s sake but because one expects that the failure report will be somehow consumed. I am not sure what would you do with the report (exception) that says that object which no longer exists failed to release its resource. I cannot think how you would respond to that.

      I am just thinking aloud; I do not claim to know what is the right thing to do here.

      • ph says:

        > I am not sure what would you do with the report (exception) that says that object which no longer exists failed to release its resource

        Me neither 🙂 at best I have ideas that fail horribly in many ways.

        Using the standard case: a function Report() Some “report” exception is thrown, causing a File object destructor to be called which fails to flush file buffers.

        The “report error” may be trivially recoverable by the caller, but the “flush” exception might require cleanup to preserve data integrity. We might *need* to consume the latter (for data consistency) but would prefer the former (for recovery, retry or diagnostics).

        When we write the File DTor, we don’t know about any of this, all we want to do is signal the problem in a way any caller can deal with it.

        If that DTor had access to the “report” exception that is in transit (so to speak), We might attach our “flush” exception to it, so the calle could probe for both – which won’t make it easier for the caller. To scale to separate libraries, std:.exception would have support such attachment, too.

        Or maybe C++ could try to support multiple exceptions “in transit”, so that

        File f(…);
        throw ReportException();
        // ~f throws FileException()

        would satisfy both catch(ReportException) and catch (FileException). Our catch blocks would need a throw_remaining_exceptions – in which case that very catch block would have to be tried again if we have multiple exceptions of the same type.

        Maybe we “can’t use exceptions this way”, we do need to handle the “flush” exception before the “report” exception leaves the Report() block. This fails, however, if File is an interface to distinct implementations that the Report() does not know how to handle (rather, how to handle errors would need to become part of the File interface).

        Lastly, don’t have a throwing destructor, force File clients to flush before closing. That somehow defeats the purpose of RAII and leads to code that reads and maintains no bit better (likely worse) than using error codes.

        • You bring up interesting points, but at this time I only want to address one statement:

          > “Lastly, don’t have a throwing destructor, force File clients to flush before closing. That somehow defeats the purpose of RAII […]”

          In my view, flushing the buffer in destructor is not RAII: you are not releasing a resource but perform some “completion logic” as in SOCI. This is a controversial (but not necessarily “bad”) usage of destructors.

          Closing the file is RAII though (at list according to my understanding of the term), and it might fail.

  4. red1939 says:

    I don’t really understand why we should “accept” throwing constructors as suggested by mgaunard. Because some programmers fail to verify the state in their program? Double exception is the end of our program, so I’d rather swallow the exception and deal with it quickly after reading the log. Failure to release one resource shouldn’t result in unconditional terminate.

  5. red1939 says:

    Which in fact disallows to use such class in RAII friendly way, which is a big issue in my book. std::thread (AFAIR) dtor will throw from dtor if you don’t join the thread. If I had to accept nothrow(false) dtor I’d rather force the user to manually call the cleanup function (dtor will validate that) and deal with the exception, should it happen.

  6. JonKalb says:

    Nice article Andrzej.
    For more on the implications of exceptions that throw see “Evil, or Just Misunderstood” http://cpp-next.com/archive/2012/08/evil-or-just-misunderstood/

  7. Alex Bath says:

    Interesting.

    There are many cases in the world of C++ where you should follow a general maxim as much as you can – but not dogmatically because the are always intelligent exceptions to the rule. That’s life. Threading is what comes to mind first so as long as its logical, reasonable and well documented you should sometimes albeit rarely throw from a destructor. Just don’t make a habit of it or worse still build it into your design as a feature!

  8. Pingback: C++: noexcept specification generated for destructors in C++11 | musingstudio

  9. matt77hias says:

    Interesting peculiarities of the C++ standard. Given that all destructors should be non-throwing by default, would you advise the explicitize “noexcept” for each destructor (for instance in the presence of 3th party libraries)?

    • I think I would not. It might make some sense for some particular cases, but as a general rule it gives too little gain compared to the “costs” of having to type the obvious things in declarations and make them longer.

      The STL requires that types passed to it must not throw excpetions from destructors, but it does not require or expect that these destructros are declared with noexcept.

  10. Chris says:

    I just made a destructor specifically for the purpose of throwing. The idea is that first making a std::ostringtream then streaming to it and finally throwing is code duplication if you do it in multiple places. So, one can use RAII to make sure it gets done. The throw is done in the destructor quite by design.

    class ThrowRuntimeError
    {
    public:
       ~ThrowRuntimeError() noexcept(false)
       {  
          throw std::runtime_error(os.str());
       }
       template <typename T>
       std::ostream &operator<<(T&& t)
       {  
          return os << t;
       }
    private:
       std::ostringstream os;
    };
    
    • I understand the motivation. Yet, it looks like you are making an implicit assumption that serializing arbitrary T into a stream cannot fail (and report this failure via an exception). If operation os << t throws, you end up in a double exception situation:

      ThrowRuntimeError()
        << 1        // ok
        << makeX() // suppose this throws
        << 0        // this is skipped
        ;           // destructor throws the second exception
                   // -> std::terminate() is called
      

      In order to avoid the problem you would have to take measures in the destructor to avoid throwing when stack unwinding is already in progress, which is possible in C++17, but a cleaner solution would be to use a function template:

      // c++17
      template <typename... T>
      [[noreturn]] void throwRuntimeError(T&&... t)
      {
         std::ostringstream os;
         (os << ... << std::forward<T>(t));
         throw std::runtime_error(os.str());
      }
      
  11. Leslie says:

    Recently Bjarne Stroustrup and others discussed this, too.
    See https://github.com/isocpp/CppCoreGuidelines/issues/1209 and the related tickets.
    They finally came up with this:

    > By explicitly marking destructors noexcept, an author guards against the destructor becoming implicitly noexcept(false) through the addition or modification of a class member.
    > …
    > So, if in doubt, declare a destructor noexcept.

    See https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c37-make-destructors-noexcept
    Cheers!

  12. Pingback: Can C++ code be valid in both C++03 and C++11 but do different things?

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.