To me Objective-C has been eye-opening: it has shown that a language can do just fine without constructors. There’s no need for all these complications and special cases. Not having constructors makes object construction more flexible and powerful.
A significant difference between C++ and Objective-C is that Objective-C zero/nil-initialises all fields on alloc, so object state is well-defined from the beginning; though if you need a field to start with a different value to ensure the invariants hold, you’re back to relying on (init…) convention. C++’s designers decided against automatic initialisation well over 2 decades ago, presumably for performance reasons; (Not backwards compatibility; they could have made this another difference between struct and class.) obviously, this has a bunch of drawbacks…
ObjC, being so dynamic, can’t do much about it. But in a C++like language the performance issue could have been avoided. The optimizer could simply see the zeroing is a redundant store and eliminate it. Or it could take Rust’s approach of having only struct literals and combine that with C++’s copy elision.
I suppose in C++ case the design went in the wrong direction already at the C stage, and C++ just dug deeper.
My parent post wasn’t a defense of C++; I’ve been using it for 20 years and it’s been my primary language for well over 50% of career earnings, so I’ve seen enough of the good, the bad, and especially the ugly, that I think it’s fundamentally flawed in various ways.
Many design decisions need to be viewed in context of their time though. (Near) full backwards compatibility with C was an enormous advantage initially, and trivial C interop still is very important now. Class & inheritance-based OOP was a big tickbox buzzword back then. Optimisers were much more basic. Rust’s compile times today are awkward on multi-gigahertz multicore monsters, imagine what it’d be like on a 486 with 33MHz and 4MB RAM. C++‘s build times were significantly slower than C’s, but for many the trade-off was worth it. Fancier, expensive to compile features would have probably tipped it too far though.
Hence my point about class vs struct. class never had any backwards compat to uphold, so they probably should have made struct the legacy POD aggregate and made classes never-POD, always-safe.
My summary: Constructors in C++ are bad because you can’t always tell which one gets called, and the only way to signal an error is to throw an exception.
The author suggests using a static member function that returns an std::optional instead. However, C++ has no exhaustive pattern matching so the code isn’t as neat as in Rust.
The article makes a lot of valid and well informed points around how C++ constructors don’t handle failure well, which they don’t. I’ve seen this modeled also by returning std::unique_ptr<T> or static bool initialize(T& t, args...) which allows uninitialized objects to be created to do “fail-able construction” but doesn’t use exceptions.
Unfortunately the use of unnecessary editorial commentary and profanity undermines the message.
C++’s standard library’s std::optional could have been done in a much safer way, by, for instance, providing you with some combinators to work with the underlying type.
In theory, you can use something like std::variant<std::monotype, T> instead of std::optional<T>, and then work with it using std::visit. This can allow you to stumble slightly further before you collapse in despair.
(This is a bit like using Either () a as an equivalent to Maybe a in Haskell. std::visit is sort-of analogous to fmap. Of course in Haskell, both Maybe and Either are Functors: in C++, I’m not aware of any std::visit equivalent for std::optional)
I found this article a bit disappointing. It has plenty of opinions disguised as facts, like “My point here is that constructors must not be used for fallible constructions.”. In fact the standard imposes no such requirement and you’re free to do this if you wish. It may or may not be a good idea. There is probably an argument to be made as to why this should be avoided but the author doesn’t really attempt it other than “Exceptions in C++ are heavy”. Which is not really that convincing here since any time this exception would be thrown indicates a logic error in your program and would most likely terminate the entire process anyway.
For an article that starts off by talking about how to enforce a non-null std::unique_ptr, the complete lack of any mention of gsl::not_null is striking. And GSL’s not_null and Expects show us that there is another way to handle precondition failures other than the ones described in this article: call std::terminate. That may seem extreme at first but when you consider that any place where this may happen indicates a logic error in the program which usually cannot be reasonably recovered from, it makes some sense. And if you want to customize this you can always use std::set_terminate.
There are some times when using a static member function to do construction makes sense. But there’s no mention of the downsides of such a choice. The type is now harder to use generically. If you have some generic function like template <typename T> T make(...) { return T(...); } you can’t use it with T=NonZero because NonZero has no public constructor. And it means that each person trying to use your type has to remember the name from_u32 in order to construct one. This is not a serious problem if you have one or two such types in your codebase but if you make every type behave like this it becomes a lot of extra names to remember.
The problems in this article are fairly well known (“The Design and Evolution of C++” talked about exceptions and failing constructors back in 1993) and there are several ways to work around them.
Some of the examples he uses aren’t very good, either. In the one with the function taking a vector of Foo, it’s trivial to avoid the copy in the C++ version by passing a const reference instead of passing by value, and that’s elementary level C++ that any C++ programmer will know. Rust and C++ have different default calling conventions, but neither way is strictly better than the other, and it’s very easy to choose the right one when writing code, so who cares?
I’m coming to the conclusion that any article that can be summed up as “$LANGUAGE_X is bad because it doesn’t handle $SITUATION_Y exactly like $LANGUAGE_Z” is more than likely a rant by somebody who’s recently learned $LANGUAGE_Z…
This article is written with the assumption that it’s bad to throw exceptions to signal failure to construct objects. It isn’t. Why would it be? Exceptions are much maligned but really really aren’t a bad thing in C++. They’re much more efficient in the good path than return-codes-with-out-parameters or optional/maybe ADT style.
To me Objective-C has been eye-opening: it has shown that a language can do just fine without constructors. There’s no need for all these complications and special cases. Not having constructors makes object construction more flexible and powerful.
A significant difference between C++ and Objective-C is that Objective-C zero/nil-initialises all fields on
alloc
, so object state is well-defined from the beginning; though if you need a field to start with a different value to ensure the invariants hold, you’re back to relying on (init…
) convention. C++’s designers decided against automatic initialisation well over 2 decades ago, presumably for performance reasons; (Not backwards compatibility; they could have made this another difference betweenstruct
andclass
.) obviously, this has a bunch of drawbacks…ObjC, being so dynamic, can’t do much about it. But in a C++like language the performance issue could have been avoided. The optimizer could simply see the zeroing is a redundant store and eliminate it. Or it could take Rust’s approach of having only struct literals and combine that with C++’s copy elision.
I suppose in C++ case the design went in the wrong direction already at the C stage, and C++ just dug deeper.
This:
could have been this from the start:
My parent post wasn’t a defense of C++; I’ve been using it for 20 years and it’s been my primary language for well over 50% of career earnings, so I’ve seen enough of the good, the bad, and especially the ugly, that I think it’s fundamentally flawed in various ways.
Many design decisions need to be viewed in context of their time though. (Near) full backwards compatibility with C was an enormous advantage initially, and trivial C interop still is very important now. Class & inheritance-based OOP was a big tickbox buzzword back then. Optimisers were much more basic. Rust’s compile times today are awkward on multi-gigahertz multicore monsters, imagine what it’d be like on a 486 with 33MHz and 4MB RAM. C++‘s build times were significantly slower than C’s, but for many the trade-off was worth it. Fancier, expensive to compile features would have probably tipped it too far though.
Hence my point about
class
vsstruct
.class
never had any backwards compat to uphold, so they probably should have madestruct
the legacy POD aggregate and madeclass
es never-POD, always-safe.My summary: Constructors in C++ are bad because you can’t always tell which one gets called, and the only way to signal an error is to throw an exception.
The author suggests using a static member function that returns an
std::optional
instead. However, C++ has no exhaustive pattern matching so the code isn’t as neat as in Rust.The article makes a lot of valid and well informed points around how C++ constructors don’t handle failure well, which they don’t. I’ve seen this modeled also by returning
std::unique_ptr<T>
orstatic bool initialize(T& t, args...)
which allows uninitialized objects to be created to do “fail-able construction” but doesn’t use exceptions.Unfortunately the use of unnecessary editorial commentary and profanity undermines the message.
In theory, you can use something like
std::variant<std::monotype, T>
instead ofstd::optional<T>
, and then work with it usingstd::visit
. This can allow you to stumble slightly further before you collapse in despair.(This is a bit like using
Either () a
as an equivalent toMaybe a
in Haskell.std::visit
is sort-of analogous tofmap
. Of course in Haskell, both Maybe and Either are Functors: in C++, I’m not aware of anystd::visit
equivalent forstd::optional
)I found this article a bit disappointing. It has plenty of opinions disguised as facts, like “My point here is that constructors must not be used for fallible constructions.”. In fact the standard imposes no such requirement and you’re free to do this if you wish. It may or may not be a good idea. There is probably an argument to be made as to why this should be avoided but the author doesn’t really attempt it other than “Exceptions in C++ are heavy”. Which is not really that convincing here since any time this exception would be thrown indicates a logic error in your program and would most likely terminate the entire process anyway.
For an article that starts off by talking about how to enforce a non-null
std::unique_ptr
, the complete lack of any mention ofgsl::not_null
is striking. And GSL’snot_null
andExpects
show us that there is another way to handle precondition failures other than the ones described in this article: callstd::terminate
. That may seem extreme at first but when you consider that any place where this may happen indicates a logic error in the program which usually cannot be reasonably recovered from, it makes some sense. And if you want to customize this you can always usestd::set_terminate
.There are some times when using a static member function to do construction makes sense. But there’s no mention of the downsides of such a choice. The type is now harder to use generically. If you have some generic function like
template <typename T> T make(...) { return T(...); }
you can’t use it withT
=NonZero
becauseNonZero
has no public constructor. And it means that each person trying to use your type has to remember the namefrom_u32
in order to construct one. This is not a serious problem if you have one or two such types in your codebase but if you make every type behave like this it becomes a lot of extra names to remember.The problems in this article are fairly well known (“The Design and Evolution of C++” talked about exceptions and failing constructors back in 1993) and there are several ways to work around them.
Some of the examples he uses aren’t very good, either. In the one with the function taking a vector of Foo, it’s trivial to avoid the copy in the C++ version by passing a const reference instead of passing by value, and that’s elementary level C++ that any C++ programmer will know. Rust and C++ have different default calling conventions, but neither way is strictly better than the other, and it’s very easy to choose the right one when writing code, so who cares?
I’m coming to the conclusion that any article that can be summed up as “$LANGUAGE_X is bad because it doesn’t handle $SITUATION_Y exactly like $LANGUAGE_Z” is more than likely a rant by somebody who’s recently learned $LANGUAGE_Z…
This article is written with the assumption that it’s bad to throw exceptions to signal failure to construct objects. It isn’t. Why would it be? Exceptions are much maligned but really really aren’t a bad thing in C++. They’re much more efficient in the good path than return-codes-with-out-parameters or optional/maybe ADT style.