1. 14

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. 7

    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. 2

      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.

      1. 1

        I think this situation would be avoided in C++ if it allowed either an initializer list, or a constructor body, but not both. As a rule of thumb, if I already have an initializer list, and I need to do something in the body of my constructor, I do my best to factor that into a separate class.

    2. 4

      (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. 4

        I think the two most important things to get about Object Oriented Design, are things I will admit I only got to years too late….

        Classes are all about preserving the invariant. https://www.artima.com/intv/goldilocks3.html

        And a constructor is all about gluing a bunch of subobjects (or POD’s) together so that you are guaranteed that whenever you have an instance of that type, you can henceforth entirely rely on the invariant being true.

        The invariant expression is Implicitly part of the precondition whenever you have an input of that type, and implicitly part of the postcondition when you output one.

        The other thing I learnt way way too late (no, I’m not a particular slow learner, text books and tutorials are slower, and they don’t update nor do they update there readers)

        http://www.drdobbs.com/cpp/how-non-member-functions-improve-encapsu/184401197

        A bit mind blowing that one.

        How do you decide whether code belongs in an instance method or not?

        Answer: If and only if it needs to operate on the guts of the object sufficiently much that the invariant will, for a moment in time, no longer hold AND this operation cannot be achieve by using other simpler public methods.

        Otherwise make it non-friend, non-member.

        ie. Object Instances methods are not the places where you do the work.

        They are thin shims that allow you to do Design by Contract with a powerful set of pre and post conditions enforced with the help of the compilers type system.

        So what do I regard these days as belonging in a constructor? Well in C++ my constructors usually have a member initializer list and an empty body.

        In ruby, often the initialize the instance variables, and then at the end I invoke freeze.

        If, as I carry on developing the class, I really feel the need to mutate the instance, I go back and remove it. Usually I find I don’t need to.

        If I’m maintaining legacy code, the first thing I do is put a freeze in at the end of every constructor. And the run the unit test suite.

        Very informative. Curiously enough, I never find I need to remove all freeze’s I put in.

        1. 1

          Thanks for those links! I will note a tension between those two pieces of writing: what Scott Meyers writes is in opposition to what Stroustrup advocates. Basically Meyers wants increased complexity at the outset as a hedge against future requirements changes. Stroustrup wants us not to over engineer at the start.

          1. 1

            Personally I think they are in perfect accord.

            The one and only thing that matters is the class invariant.

            If the code is not about preserving the class invariant, it doesn’t belong in the class.

            If the data isn’t involved in the invariant it doesn’t belong in a class.

      2. 2

        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. 2

          This topic comes up from time to time on /r/cpp – https://www.reddit.com/r/cpp/comments/938ncv/what_is_the_most_useful_software_design_pattern/e3by0j1/ is a recent occurrence.

          Of the alternatives to RAII that have been proposed, CADRe (Constructor Acquires, Destructor Releases) is the best one I’ve come across. What I don’t like about it is that it implies that classes should have user-defined destructors, but in general, we use RAII so that we can avoid writing destructors.

          I like the notion of constructors as name binders (or really initializer lists as name binders) – as the initializer list is evaluated, each sub-object is constructed and then bound. This has other functional connotations as well – before C++11 gave us lambdas, objects were our closures. If you want the idea to catch on, though, I feel it needs to be framed in a way that can be digested by programmers not familiar with functional programming.

          1. 2

            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. 4

              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. 6

                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.

              2. 1

                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.

                I don’t speak parsel tongue, but if I recall correctly it’s a bit like Ruby on that front.

                ie. The use of garbage collection means resource release can be indefinitely delayed, making RAII an unsuitable idiom.

                The ruby equivalent is an “acquire resource/begin/yield resource/ensure release resource/end” sequence.

                I think, I may be wrong, the python equivalent is a try finally sequence.

                Alas, programmers are lazy, I often see cases where an exception for entirely other reasons results in a trip through the ensure(finally) block which will attempt to release a resource not yet acquired…

                Personally I still think exceptions where the wrong mechanism. Rust’s tagged unions are actually pretty good.

                recommend reading the exceptional C++ books by Herb Sutter

                Which are full of fine fine fine print about how you can cut yourself with them if you miss a trick. Part of the reason for my recommendation above is you avoid having to review your code line by line against a laundry list of fine print.

              3. 1

                Not as catchy, but far less problematic.

                Well, you didn’t throw anything we could catch, so yeah…

                1. 2

                  Well throwing things you couldn’t catch is just not the way I was raise’d…. but I can retry just for you as an exception.