On 8/23/2018 3:12 PM, David Nadlinger wrote:
On Thursday, 23 August 2018 at 21:31:41 UTC, Walter Bright wrote:
My personal opinion is that constructors that throw are an execrable programming practice, and I've wanted to ban them. (Andrei, while sympathetic to the idea, felt that too many people relied on it.) I won't allow throwing constructors in dmd or any software I have authority over.

Throwing constructors are fundamental for making RAII work in a composable 
fashion.

I understand my opinions on this diverge from the conventional wisdom.


If constructors are not allowed to throw and you want to avoid manually creating a "uninitialized" state – which is error-prone and defeats much of the point of an RAII strategy –, all dependencies need to be injected externally, that is, constructed independently and then passed into the constructor. Sometimes, inversion of control is of course the right call – cf. the hype around DI –, but sometimes you'd rather cleanly abstract the implementation details away.

Banning them from the language only pushes the complexity of handling semi-constructed objects into ad-hoc user code solutions, which I'd argue is worse in terms of usability and potential for bugs.

I suppose you view this as advantageous because you place more weight on the language not having to explicitly deal with this scenario in the text of the specification?

It's easy to specify. That's not an issue at all. It's also easy to implement - where my PR for it failed, was it broke existing code that should never have compiled anyway (for example, a nothrow constructor would then call a throwing destructor, or an @safe constructor now would call an unsafe destructor). Dealing with this likely means a compiler switch so an upgrade path is easier.

Let's deal first with the easy case - throwing destructors. The problem is that unwinding the stack to deal with the exception means calling destructors. You then have the infamous "double fault exception", and C++ deals with it by terminating the program, which is hardly useful.

D deals with it via "chained exceptions", which is terrifyingly difficult to understand. If you believe it is understandable, just try to understand the various devious test cases in the test suite. I regard D's chained exceptions as an utter failure.

Back to throwing constructors.

1) They are expensive, adding considerable hidden bloat in the form of finally blocks, one for each constructing field. These unwinding frames defeat optimization. The concept of "zero-cost exception handling" is a bad joke. (Even Chandler Carruth stated that the LLVM basically gives up trying to optimize in the presence of exception handlers.) Herb Sutter has a recent paper out proposing an alternative, completely different, error handling scheme for C++ because of this issue.

2) The presence of constructors that throw makes code hard to reason about. (I concede that maybe that's just me.) I like looking at code and knowing the construction is guaranteed to succeed. Somehow, I've been able to use C++ for decades without needing throwing constructors. Let's take the canonical example, a mutex:

  {
    Mutex a; // acquires a mutex, throws if it fails
    ... locked code ...
  } // mutex is released

My suggestion:

  {
    Mutex a; // creates a mutex
    a.acquire(); // does the obvious thing
    ... locked code ...
  } // mutex is released

It's still RAII, as the destructor checks to see if the Mutex is required, and if so, releases it.

You might argue "my code cannot handle that extra check in the destructor." Fair point, but stack that against the cost of the EH bloat in the constructor, and (for me) the inherently unintuitive nature of a constructor that tries to do far too much.

3) Much of the utility of throwing constructors in C++ comes from "what if the constructor fails to allocate memory". In D, out of memory errors are fatal, no recovery is necessary. That pushes any requirement for throwing constructors to the fringes in the first place.

Reply via email to