Jens Maurer
2013-01-09

Differentiating destructor invocation contexts

Motivation

Herb Sutter's discussion of std::uncaught_exception() concludes "Unfortunately, I do not know of any good and safe use for std::uncaught_exception. My advice: Don't use it."

The technical reason (aside from the moral objection) is the inability to differentiate between "this destructor was directly called by stack unwinding" and "stack unwinding is ongoing, but this destructor was called from a regular scope exit". However, sometimes exactly this differentiation would be helpful to perform additional cleanup specific to stack unwinding only. Example:

  struct Transaction {
    Transaction();
    ~Transaction() {
      if (std::uncaught_exception())
        RollBack();
      else
        Commit();
    }
  };
The well-intended meaning was for a transaction to roll back if its scope was exited via an exception. However, this detection is unreliable for the following usage:
  U::~U() {
    try {
      Transaction t( /*...*/ );
      // do work
    } catch( ... ) {
      // clean up
    }
  }
If U::~U() is called during stack unwinding, the transaction inside will always roll back, although "commit" was expected. Note that the transaction construct could appear in a called function that is not (and should not be) aware of the context from which it is called.

Approach 1: Add a second destructor called for stack unwinding

A class can have two destructors, one that is called during stack unwinding and another one that is called for regular cleanup. This also fits well with today's implementations of stack unwinding, where a distinct code path from normal scope exit is used.
struct S {
  ~S();    // regular destructor
  ~S(int); // unwinding destructor
};
Features: The transaction example could then be written like this:
  struct Transaction {
    ~Transaction()
    {
      Commit();
    }

    ~Transaction(int)
    {
      RollBack();
    }
  }; 
Open issues:

Approach 2: Differentiation by destructor parameter value

(General idea by Evgeny Panasyuk.) A new kind of destructor taking a bool parameter conceptually replaces (not: complements) the existing destructor syntax. A class can either have a traditional destructor without any parameters, or one that takes a bool parameter. Example:
struct S {
  ~S(bool unwinding);  // called with argument value "true" if unwinding
};
Features: The transaction example could then be written like this:
  struct Transaction {
    ~Transaction(bool unwinding)
    {
      if (unwinding)
        RollBack();
      else
        Commit();
    }
  }; 
Open issues:

Approach 3: uncaught_exception_count()

The exception implementation maintains a count of exceptions currently causing stack unwinding. If the count is the same during construction as during destruction, the destruction was caused by a regular scope exit, otherwise it is due to stack unwinding. See also Evgeny Panasyuk's library. Example:
struct S {
  unsigned int count = std::uncaught_exception_count();
  ~S() { /* unwinding if count < std::uncaught_exception_count() */ }
};
Features: The transaction example could then be written like this:
  struct Transaction {
    const unsigned int count = std::uncaught_exception_count();
    ~Transaction()
    {
      if (count < std::uncaught_exception_count())  // unwinding
        RollBack();
      else
        Commit();
    }
  }; 
The memory footprint can be made as small as one bit:
struct S {
  bool latch : 1;
  S() : latch(std::uncaught_exception_count() & 1) { }
  ~S() { /* unwinding if latch != std::uncaught_exception_count() & 1 */ }
};

Discussion

Existing code will continue to work as before; only new code expressly using the new feature will be affected. No additional keyword is consumed. The "int" parameter for the unwinding destructor is motivated by a roughly similar use for overloaded increment and decrement operators; see 13.5.7 over.inc.

The ~S(int) idea seems to be inferior, because the presence of an ~S(int) destructor changes the possible call contexts and thus the implied meaning of an already existing ~S() destructor. Factoring common code in the destructor becomes more involved. Also, the wording changes appear to be more involved.

The ~S(bool) idea seems to imply quite a few wording changes for a feature only used in fringe cases. The std::uncaught_exception_count() approach seems to localize and minimize the required changes.

The mechanisms presented above are only intended for objects with automatic storage duration. For static, thread, or dynamic storage duration, the semantics should be well-defined, but no particular add-on value should be expected from the new feature (regardless of approach).