1. 12

A generation of programmers have been mislead down a deep rabbit hole thinking that “Constructors” are things that “Construct” objects.

This has to led to a generation of vaguely smelly code that “does too much work in the constructor” (of which throwing exceptions is evidence).

The last few years I have told myself (and anyone who doesn’t back away fast enough) that “Constructors” do not construct objects, they are “Name Binders.” (Sort of like lisp’s “let” macro)

They bind instance variable names to pre-existing sub-objects.

This attitude, coupled with an a rule of thumb, “make it immutable unless I prove to myself that I need it to be mutable” has led to a major improvement in my code.

Of course, the place where this whole issues rages hardest is around RAII and constructors throwing exceptions.

If one realises that in most instances Failure to Acquire a Resource Is Not An Exceptional Circumstance, in fact, it’s perfectly business as usual, then throwing an exception is not good design.

So RAII should be…

“Taking Ownership of an Acquired Resource is Initialization, and relinquishing ownership is automatic at the object life time end, but Failure to Acquire a Resource Is Not An Exceptional Circumstance”

Not as catchy, but far less problematic.

Naturally calling them Destructor’s is equally problematic. Relinquishers might be a better name.

  1.  

  2. 6

    I think Go and Rust did a lot of work towards helping people think about construction, by not having it!

    Both allow you to instantiate a struct by it’s fields. If you want to do something more complicated, you write a function, with a normal signature.

    1.  

      And this is what you end up doing even in C++ if you an initialization routine that can fail: a static function that constructs an object or fails. You mark the constructor as private so it can only be created with these static functions.

      The problem with this approach is that it leaves inexperienced programmers with another gun to shoot themselves with. If they neglect to handle failure cases in the constructor, the object can get into a state that was not reasoned about.

      Best to keep it simple by having only one way to create objects, like in Go and Rust.

    2.  

      (I think this is a cool development - lobste.rs here is not linking to an article, but serving as a forum where people write articles itself)

      @JohnCarter, could you elaborate a bit please: what other things in a constructor do you consider making it too big?

      Are there global side effects? Like passing in references/pointers to other objects and the constructor is mutating them? That indeed sounds like spaghetti code.

      Are you talking about just the size of the code? I can think of cases where a large class, composed of many smaller classes, would end up doing a bunch of initialization, and so you would have a large constructor.

      In the little work that I’ve done, I’ve never needed particularly verbose constructors, especially since I try to rely on default constructors of objects that compose the enclosing class.

      To speak to your note about exceptions, I’ve been taught that if your object can fail on construction then you should be supplying a construct function that raises the exception or returns an invalid object (e.g. through std::optional)

      1.  

        It’s interesting to notice that Smalltalk had this right from the start. Constructors are a convention and they are methods on the class rather than the instance. That might not seem important, but having this “factory” level as a syntactically simple convention allows clean separation of the work needed to create an instance, work that could fail on its own, and the normal invariants that you’d have for an object when it is ready to use.

        1.  

          To some extent I agree, however if there is too much code in the constructor it also is quite often an indicator that the code should be refactored. The convenience of what the RAII principle is providing allows also to avoid coding and resource management mistakes/bugs. In c++ exceptions are considered to be used for exceptional circumstances, and they are quite expensive for binary size, at least I made that experience when I used to work on some bigger applications. On the other hand I have been writing a lot of python code in the last few years and exceptions are the defacto way to handle errors. And it’s very much not a problem to use exceptions for non exceptional use cases.

          In C++ I rather would argue that the approach of acquiring resources during initialization makes sense because it reduces your need for introducing checks that could lead to wrong code paths especially in the destruction. When applied correctly your program will be more robust if you use the raii principle. I recommend reading the exceptional C++ books by Herb Sutter those are very much still one of the best sources in writing C++ code that is handling exceptions properly.

          Complexity in constructors and destructors is something to avoid which is something as I understand is your main criticism to the principle.

          1.  

            it’s very much not a problem to use exceptions for non exceptional use cases.

            Agreed. Many dynamic programming problems are a good fit for backtracking with longjmp/“exceptions” which is how you’d write it in assembly anyway.

            Lately (well, sometime in the last couple decades) I’m thinking “exceptions” aren’t a good use for exceptional situations either. That is to say, we’re using exceptions exactly backwards.

            One of the best examples I’ve come up with is the out of disk space error. When you’re out of disk space, many programs will give up/crash – but consider the ergonomics of a CAD or video editing tool, that upon save starts moving this multi-gigabyte asset out to disk over what’s perhaps several minutes and then – oops! that partition is full.

            If we unwind, the (temporary) file needs to be cleaned up(deleted), the user informed to make room somehow – none of these tasks are straightforward – all the file opens to save this large asset need to be in on it, which is often tricky if the file-saving was done by a different team than the UI team (who handles the exception, and thus deletes the files).

            But what if we use a handler? We call the handler, from deep in the file saving code, and tell it “we’re out of disk space”. We can easily offer a callback to “retry”, and a list of any (temporary) file created so far so that they can be deleted (or moved onto another disk, or whatever). This could be provided by the UI team, but if not the operating system could have a crude “abort retry fail” dialog. We live in a multitasking world these days, so that’s probably good enough for a lot of enterprise applications – use goes and moves those files around and then resumes the save (by returning from the handler).

            1. 5

              That is exactly the approach taken by Common Lisp’s condition system (which is used for more than just errors, but also for general notifications. It’s extremely powerful, and an illustration of how Lisp really is an industrial-strength language for real problems.