Rendered at 09:39:25 GMT+0000 (Coordinated Universal Time) with Cloudflare Workers.
MiroslavPokorny 1 days ago [-]
This is broken. Theres no common-good in allowing invalid objects to be created. Constraints and validation are important in all parts of a system, and ctors is just one of the units that also need to perform checks to ensure this is true.
quietbritishjim 1 days ago [-]
Absolutely agreed.
If you really wanted to go down the author's route, then you'd at least make the constructor private, and have a static method that returns a valid constructed object or an error, so you never have e.g. an "unconnected" database connection:
But I wouldn't even bother with this - it's exceptions with extra steps. I seem to remember that Stroustrop said in the Design and Evolution of C++ that the main reason for adding exceptions in the first place was to allow a failure result from a constructor.
quuxplusone 1 days ago [-]
> The C++ container classes assume noexcept constructors for this reason, because creating a list of size 20 calls your constructor 20 times, and the cleanup gets so much messier.
No, STL containers such as std::vector definitely do not assume (nor require) non-throwing constructors. They just do the right thing. Yes, cleanup is messier than it would otherwise be.
STL containers do require that your destructors not throw (but do not require that they be noexcept).
quietbritishjim 1 days ago [-]
Containers also give more efficient and simpler semantics if move constructors are noexcept, which they generally should be.
quuxplusone 8 hours ago [-]
std::vector does. No other container cares about noexcept at all.
They could be even more efficient if we had a way to indicate relocatability instead of having to fall back to more general move semantics.
xg15 15 hours ago [-]
> The C++ container classes assume noexcept constructors for this reason, because creating a list of size 20 calls your constructor 20 times, and the cleanup gets so much messier.
I find it interesting that programming newbies are usually scared of C/C++ because "you have to deal with pointers" unlike in Java, Python, JavaScript etc.
It's interesting because all those languages of course use pointers and heap-allocated objects. The thing that sets the other languages apart is that they only allow pointer access and thus can make the language easier to reason about.
So what's "unusual" in C, C++ and Rust is really the parts that are not pointers, i.e. the ability to manage a data structure and its layout in memory directly, with all its consequences.
msm_ 11 hours ago [-]
I don't think this is the crux of the matter. Programming newbies are not scared of C/C++ because of std::unique_ptr<T>. They are scared of three-star programmers, of intrusive lists, of clever packed data structures - in short, of pointer arithmetic. Java, Python and JavaScript don't allow that, so it's hard to compare them (that's why we distinguish between "pointer" and "reference", even though in practice they are exactly the same thing under the hood, of course).
x-complexity 1 days ago [-]
Constructors are dedicated language-sanctioned functions that transform (data inputs X) into (type/object Y).
The given X should be subjected to the requirements necessary to create a valid Y. If the requirements fail, you have two main choices: Create an invalid Y, or reject X & return an error in however you want it to be returned.
The former leaks said rejections throughout the rest of the type's/object's code, & makes more work for me to parse through where & how the problem occurs.
The latter option creates a cleaner system altogether, due to rejecting sections of invalid states. Not all will be rejected, but the fact that some do get rejected already makes its workload comparatively lighter than the former.
I know which my lazy bum would pick.
Panzerschrek 1 days ago [-]
> Turn your Database class into a state machine with an uninitialized and a connected state. Then have a Database.connect()
It's a bad advice. It's preferred instead to have a connect function returning a database instance in a successful case or an error otherwise.
account42 20 hours ago [-]
Is it? Your database connection can fail at any time even if initially successful so what's the advantage of having another separate error path for the initial connection.
oraro 24 hours ago [-]
Trying to keep an open mind here–if your constructor arguments are plain wrong then who should be throwing the error then? The Factory? The Builder? And what if they don't exist in the first place?
robotresearcher 15 hours ago [-]
Ideally you’d use the type system to ensure that arguments can’t be wrong.
For example, if arg values can’t be negative, use an unsigned arg type.
AnimalMuppet 1 days ago [-]
> Because the semantics of a constructor is to bring an object into a valid state and you don’t expect it to perform any functionality.
No, I expect it to perform the functionality of bringing the object into a valid state - whatever functionality that takes. And if it can't, then throwing is the next option, because it's preferable to having an object in an invalid state.
robotresearcher 14 hours ago [-]
What if the object can be invalidated asynchronously after the constructor has completed and before you get to use the object? For example by a remote connection dropping? You have to check that it still works anyway: you can’t assume it by construction.
I prefer to have constructors that can’t go wrong (except trivially for OOM that ends the whole process), and then manage their dynamic state after construction.
AnimalMuppet 13 hours ago [-]
Well, what if a valid object has to have a semaphore, because it can be accessed by things from multiple threads, and the OS has a limited number of semaphores that it will create? The object won't be in a valid state without a valid semaphore, so the constructor can fail. (Or, if you want, the semaphore's constructor can fail, but that will mean that this object's constructor also fails.)
Look, for your problem, you probably want to do what the fine article said - to have "invalid connection" be a valid state of the object.
robotresearcher 12 hours ago [-]
I’d initialize the semaphore and any other things that could fail in a post-constructor setup/startup method. It’s a little ugly and it violates RAII, but I think it’s worth it on balance. So yes, I’m agreeing with you that’s how I’d do it.
AnimalMuppet 10 hours ago [-]
I've done it that way at least once. But, personally, I prefer RAII, rather than a "half-constructed" state.
DemocracyFTW2 1 days ago [-]
> Constructors Shouldn't Throw
wrong
verdverm 1 days ago [-]
this is not helpful, please explain why you think this
If we interpret "constructor" more generically as setup, one of my preferred languages, Go, does not have constructors, but we do have similar setup functions, which typically return errors, and we handle them as soon as possible without breaking out from the designed control flow.
stevekemp 1 days ago [-]
In go there are the "init" functions, but those do not return a possible error result.
I guess you're thinking more of package constructor methods, such as NewFoo, which tend to return the new object and an error value.
verdverm 1 days ago [-]
yes, I use init sparingly and generally panic, for low level program startup, versus the NewFoo functions, which are for dynamic objects the way constructors are used, I use NewFoo -> (Foo, error) for internal runtime, client, and configuration handling as well
20 hours ago [-]
DemocracyFTW2 1 days ago [-]
I think elsewhere in this thread the point has already been made so I thought to spare the Ink. A constructor is essentially a function that returns a value. There's little reason to think that constructors are more than that. A few examples: in JS, a constructor is a method called `constructor()`. It's a bit special in that it's the method that is privileged to be the one called when the class name is preceded by the keyword `new`, but that's almost about it. It can be replaced or supported by one or more factory functions, and this is what happened with NodeJS's `Buffer` class and generals JS's `Array` class. In Ruby, the constructor is just a method called `new()`, and I guess it's not special beyond there being a convention to construct new instances with a method called `.new()`. Like any function, a constructor has an input and an output value. For example, in `new Point 3, 4` the input is the pair of numbers `( 3, 4 )` (here written spread out, but it doesn't matter), and its output is an instance of class `Point`. Say you have a class List that accepts as only parameter the length (number of elements) of the `List`. When I call a function `build_list length: -4` I expect the `length` argument to be suitably interpreted, or an error to be thrown. Most of the time a list with a negative number of elements does not make a lot of sense, so the natural thing is to indicate an error, which in many current languages is done by throwing an exception so as to prevent silent failures. When I call a constructor as `new List length: -4` I naturally expect the exact same considerations to apply.
verdverm 1 days ago [-]
neither should destructors, my first internship was at Bloomberg and I put a BIG into a crash loop with this
quietbritishjim 1 days ago [-]
Ah, the "big". That's some fun insider knowledge!
At one point they needed a custom build of AIX because its executable size limit of 1GB wasn't enough for the big. This was about 20 years ago, I suppose that seems quaint now.
verdverm 1 days ago [-]
when I was there, the bigs were 4 gig, ~2009±1
for others context, bigs were the compiled binary deployed to a server, a composition of BB terminal functions, bin packing style
If you really wanted to go down the author's route, then you'd at least make the constructor private, and have a static method that returns a valid constructed object or an error, so you never have e.g. an "unconnected" database connection:
But I wouldn't even bother with this - it's exceptions with extra steps. I seem to remember that Stroustrop said in the Design and Evolution of C++ that the main reason for adding exceptions in the first place was to allow a failure result from a constructor.No, STL containers such as std::vector definitely do not assume (nor require) non-throwing constructors. They just do the right thing. Yes, cleanup is messier than it would otherwise be.
STL containers do require that your destructors not throw (but do not require that they be noexcept).
https://stackoverflow.com/questions/66459162/where-do-standa...
Background: https://quuxplusone.github.io/blog/2022/08/26/vector-pessimi...
I find it interesting that programming newbies are usually scared of C/C++ because "you have to deal with pointers" unlike in Java, Python, JavaScript etc.
It's interesting because all those languages of course use pointers and heap-allocated objects. The thing that sets the other languages apart is that they only allow pointer access and thus can make the language easier to reason about.
So what's "unusual" in C, C++ and Rust is really the parts that are not pointers, i.e. the ability to manage a data structure and its layout in memory directly, with all its consequences.
The given X should be subjected to the requirements necessary to create a valid Y. If the requirements fail, you have two main choices: Create an invalid Y, or reject X & return an error in however you want it to be returned.
The former leaks said rejections throughout the rest of the type's/object's code, & makes more work for me to parse through where & how the problem occurs.
The latter option creates a cleaner system altogether, due to rejecting sections of invalid states. Not all will be rejected, but the fact that some do get rejected already makes its workload comparatively lighter than the former.
I know which my lazy bum would pick.
It's a bad advice. It's preferred instead to have a connect function returning a database instance in a successful case or an error otherwise.
For example, if arg values can’t be negative, use an unsigned arg type.
No, I expect it to perform the functionality of bringing the object into a valid state - whatever functionality that takes. And if it can't, then throwing is the next option, because it's preferable to having an object in an invalid state.
I prefer to have constructors that can’t go wrong (except trivially for OOM that ends the whole process), and then manage their dynamic state after construction.
Look, for your problem, you probably want to do what the fine article said - to have "invalid connection" be a valid state of the object.
wrong
If we interpret "constructor" more generically as setup, one of my preferred languages, Go, does not have constructors, but we do have similar setup functions, which typically return errors, and we handle them as soon as possible without breaking out from the designed control flow.
I guess you're thinking more of package constructor methods, such as NewFoo, which tend to return the new object and an error value.
At one point they needed a custom build of AIX because its executable size limit of 1GB wasn't enough for the big. This was about 20 years ago, I suppose that seems quaint now.
for others context, bigs were the compiled binary deployed to a server, a composition of BB terminal functions, bin packing style