On Thursday, 23 August 2018 at 23:27:51 UTC, Walter Bright wrote:
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 don't think that assessment is accurate. Yes, I ported EH to a few new targets and wrote the first correct implementation of exception chaining for LDC, so I'm probably a couple of standard deviations off the average D user in that regard. But said average D user doesn't care about most of the nuances of this problem, like the details of implementing exception chaining without allocating too much, or which exceptions take precedence in various intentionally twisted test cases.
What they do care about is that the fact that an error has occurred is propagated sensibly in all cases without requiring extra attention, and that information as to the origin is not lost (hence chaining rather than just replacement). Heck, the fact that we still don't have default backtrace handlers that consistently work on all platforms is probably a much bigger problem than the minutiae of exception chaining behaviour.
All this is not to say that nothrow constructors aren't a good idea, though.
———
1) They are expensive, adding considerable hidden bloat in the form of finally blocks, one for each constructing field. These unwinding frames defeat optimization. […]
This cost is inherent to the problem, at least as long as exceptions are used to represent the error conditions in question in the first place. Whether the potentially throwing operations are performed in the constructor or in another method, either way the object will need to be destructed and all preceding initialisation steps undone when an error occurs.
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.
This might be just you indeed. How are constructors different than other method calls in that regard? (Granted, constructors can be called implicitly in C++.)
Somehow, I've been able to use C++ for decades without needing throwing constructors.
How much of what would be considered "modern" C++ code have you written in that time, as opposed to C-with-classes style?
It [having a separate initialisation method]'s still RAII
One might wonder about the acronym then, but whether your example should be called RAII is an entirely different debate, and one I'm not particularly interested in (I've always thought something like "Resource Release Is Destruction" or just "Scope-based resource management" would be a better name anyway).
You might argue "my code cannot handle that extra check in the destructor."
It's not just one extra check in the destructor. It's an extra check in every member function, to throw an exception if called on an object that has not been initialised.
You might argue that these should be errors/assertions instead, and hence are not as expensive. True, but this points to another problem: Splitting up the initialisation procedure invites a whole class of bugs that would otherwise be syntactically impossible to write.
There is considerable theoretical and practical beauty to the idea that as an object is accessible, it is in a valid state. (Linear/affine types, cf. Rust, show the power of this notion extended to ownership.) RAII in the conventional sense (acquisition in the constructor) effectively provides a way to encode 1 bit of typestate. By blurring the line between object initialisation and the phase where it is fully initialised, that descriptive power is lost.
[…] (for me) the inherently unintuitive nature of a constructor that tries to do far too much.
I suppose our opinions just differ here then. To me, this is exactly what I intuitively expect a constructor to do: Create an instance of an object in a normal state. Drawing the line anywhere else, for example (implicitly) allocating memory but not initialising it to a fully usable state, seems artificial to me.
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.
This is unrelated to the discussion at hand, but std.experimental.allocator does allow handle allocation failure, and with good reason: It doesn't make sense to abort the program when a fixed-size local cache or a static global object pool (common in embedded programming) is exhausted.
— David