https://jmmv.dev/2021/11/cpp-ctors-vs-init.html # Julio Merino * About * Essays * Resume * Software * Archive * Privacy * Series * Tags [ ] [search] Constructors and evil initializers in C++ November 24, 2021 * About 7 minutes * Tags: c++, readability One of the three key tenets of Object Oriented Programming (OOP) is encapsulation: objects contain a state that, when observed from the outside, is always internally-consistent. To illustrate this, suppose you have a class to represent a rectangle, and that this class tracks the rectangle's dimensions as well as its area: class Rectangle { int width, height, area; public: Rectangle(int w, int h) : width(w), height(h), area(w * h) {} std::pair dimensions() const { return std::make_pair(width, height); } int area() const { return area; } void resize(int w, int h) { width = w; height = h; area = w * h; } }; The Rectangle class above has two fields to represent its dimensions (width and height) and a derived field to represent its area, which we store in the object itself because it happens to be very expensive to compute (narrator: it really is not). If we use this class via its public interface shown above (in a non-threaded environment), we can guarantee that area is always up-to-date with width and height. When the object is first constructed, all fields are in a consistent state, and if we call resize() at a later stage, we know that all fields are also consistent right before and right after the method call. But note that this is not true within the class implementation: there is a point in time in the constructor and in the resize() method where the precomputed value of area is stale. And that's OK from an encapsulation perspective: we only need to worry about internal consistency at the public boundaries of the class. Let's add error handling Suppose we want to update our Rectangle class to ensure that width and height are positive and not zero. Easy-peasy: class Rectangle { // ... public: Rectangle(int w, int h) { if (w <= 0 || h <= 0) { throw std::runtime_error("Dimensions must be positive and non-zero"); } width = w; height = h; area = w * h; } // ... }; All good here from an encapsulation perspective. If we try to instantiate a Rectangle with invalid dimensions and use it, like this: Rectangle r(some_width, some_height); std::cout << "The area is: " << r.area() << '\n'; ... the constructor will throw an exception, the object will never have existed, and no code after that line in the same scope will run. There isn't any possibility in which we have an invalid r that is accessible from the code. Let's ban exceptions As it turns out, there are many codebases out there that ban the use of exceptions in C++ (Google's being a famous one). And if we don't have exceptions, it's impossible to return errors from a constructor. Which begs the question: how do we handle the situation above? How can we detect errors during construction and prevent invalid objects from ever being created? The common answer seems to be to add a separate initialization method (say init), like this: class Rectangle { // ... public: Rectangle() : width(0), height(0), area(0) {} bool init(int w, int h) { if (w <= 0 || h <= 0) { return false; } width = w; height = h; area = w * h; return true; } // ... }; And, in turn, this means that the class now has to be used like this: Rectangle r; if (!r.init(some_width, some_height)) { // Invalid dimensions; handle error! } std::cout << "The area is: " << r.area() << '\n'; The horror. Note the abomination we have introduced here. The caller now must go through two separate steps to create the object: one is Rectangle r, which calls the constructor, and another is the call to r.init(). Critically, the internal state of the object is now invalid across these two steps, and this inconsistent state is observable through the public interface. Oops, we have violated the encapsulation principle. Our program can now have instances of a Rectangle that are invalid (akin to null values), and code can reference them just fine. This is bad. Really, really bad. As I said a few months ago in reply to a tweet praising RAII: @jmmv on August 4th, 2021 * Replying to @awesomekling Making invalid states irrepresentable is probably the best thing one can do to improve reliability when designing a new piece of code. It's a difficult mentality shift though! 29 likes * 5 retweets * Go to Twitter thread Unfortunately, by separating construction from initialization like we did above, we have gone against this principle. Yes, we had to do it because of the constraints of our environment, but it'd be best if we didn't have to violate a core tenet of OOP. A possible solution There are ways to mitigate, but not fully resolve, this problem. My preferred way is to use a static factory method to perform the object construction and initialization along with the required error handling. Consider the following: class Rectangle { // ... Rectangle(int w, int h) : width(w), height(h), area(w * h) { assert(width > 0 && height > 0); } public: static std::unique_ptr create(int w, int h) { if (w <= 0 || h <= 0) { return null; } return std::make_unique(w, h); } // ... }; In this version, we have restored the original constructor that does not perform validation. Objects are now always constructed in a complete state, but it's again possible for those objects to contain invalid state: the caller of the constructor could supply invalid values. However, the constructor is now private, so this is sound from an encapsulation perspective; remember that we only must maintain consistency and validity at public interface boundaries. All great, but... with a private constructor, we cannot create instances of our class! To solve this, we can introduce a static factory method that either returns a new Rectangle if the parameters are valid, or a null pointer if the inputs are invalid. With this approach, the public interface of Rectangle is now properly encapsulated again: all objects returned by create() are guaranteed to be valid, and if the parameters are invalid, no object is created at all. (We still have to deal with null-ness though...) Unfortunately, this code is not equivalent to the version that used exceptions. Note that our factory method returns a heap-allocated object and is in full control of the object's creation. This has two problems: first, we have given up on stack-allocated objects because just declaring an object on the stack causes its constructor to be called; and, second, it becomes hard to deal with inheritance if some other class wants to extend Rectangle. Despite these issues, I'll take this version over violating encapsulation principles anytime--unless profiling says the use of dynamic memory is performance-critical or other coding issues prevent it. Takeaways Here are the key points I'd like you to remember from this post: 1. The single best thing you can do to increase the reliability of a program is to make invalid states impossible to represent. 2. Keep constructors "dumb": all they should be doing is assign fields. This applies irrespectively of the use of exceptions. 3. Avoid init-like methods if at all possible. If you have actual logic during object construction, put that code in a static factory method (with the bonus of a better-named "constructor"), or use dependency injection to push that logic to the caller. 4. As a corollary to the above, init-like methods that return void are useless. Do not separate construction from initialization just because it's a widespread practice in your codebase. Keep all initialization in the constructor unless it's impossible to do so. 5. If you must provide an init-like method to handle errors, do the smallest possible amount of work within it. Any fields that can be initialized in the constructor should be initialized there (because this allows you to make them const, for example). Reserve the init method for the few fields that are subject to error checking. 6. Consider adding an is_initialized boolean to the class, and assert that it is true in all methods (except in init, where you would assert that it is false). This adds overhead, both to the code and runtime, but it will help you detect the cases where you end up using a partially-initialized object--which I guarantee will happen. One final thought to conclude: the reason I really enjoy writing in Rust is because the language forces you to care about these correctness properties (via the Result type in the context of this post). If you don't know Rust but regularly code in C++, I'd strongly recommend you to learn Rust as well: you'll change the way you think about structuring your data types and algorithms, and will spot problematic patterns in C++ with ease. What did you think about this article? (Experimental) (0) (0) Reddit logo Share on Reddit Hacker News logo Share on Hacker News Twitter logo Share on Twitter Want more posts like this one? Take a moment to subscribe. [ ] Subscribe Follow @jmmv on Twitter RSS feed << Previous post All posts [20181124-s] Julio Merino Principal Software Engineer Currently @ Microsoft Follow @jmmv on Twitter RSS feed [ ] Subscribe Featured posts * EndBASIC 0.8: Now, with graphics! * EndBASIC 0.7: Hello, cloud! * Always be quitting * How does Google keep build times low? * How does Google avoid clean builds? * Unit-testing a console app (a text editor) * Windows Subsystem for Linux: The lost potential * Farewell, Google; hello, Microsoft! * Configuration files and .d directories * Bridging the web gap in EndBASIC * More... Archive * 2021 (22) + November 2021 (2) + August 2021 (2) + July 2021 (5) + June 2021 (1) + April 2021 (2) + March 2021 (2) + February 2021 (3) + January 2021 (5) * 2020 (36) + December 2020 (4) + November 2020 (5) + October 2020 (5) + September 2020 (2) + August 2020 (6) + July 2020 (2) + June 2020 (2) + May 2020 (3) + April 2020 (2) + March 2020 (2) + February 2020 (1) + January 2020 (2) * 2019 (24) + December 2019 (8) + November 2019 (6) + October 2019 (1) + September 2019 (2) + March 2019 (2) + February 2019 (3) + January 2019 (2) * 2018 (25) + July 2018 (3) + June 2018 (7) + May 2018 (2) + April 2018 (2) + March 2018 (8) + February 2018 (3) * 2017 (6) + October 2017 (1) + August 2017 (1) + July 2017 (1) + February 2017 (3) * 2016 (8) + September 2016 (1) + May 2016 (1) + April 2016 (1) + March 2016 (2) + February 2016 (1) + January 2016 (2) * 2015 (17) + December 2015 (2) + October 2015 (2) + September 2015 (3) + June 2015 (2) + May 2015 (3) + April 2015 (1) + March 2015 (1) + February 2015 (3) * 2014 (12) + November 2014 (2) + May 2014 (3) + March 2014 (1) + February 2014 (3) + January 2014 (3) * 2013 (62) + December 2013 (7) + November 2013 (7) + October 2013 (7) + September 2013 (13) + August 2013 (9) + July 2013 (10) + June 2013 (9) * 2012 (29) + October 2012 (1) + September 2012 (1) + August 2012 (3) + July 2012 (2) + June 2012 (2) + May 2012 (3) + April 2012 (1) + March 2012 (1) + February 2012 (10) + January 2012 (5) * 2011 (60) + December 2011 (4) + November 2011 (4) + October 2011 (5) + September 2011 (11) + August 2011 (6) + July 2011 (4) + June 2011 (6) + May 2011 (6) + April 2011 (5) + March 2011 (2) + January 2011 (7) * 2010 (26) + December 2010 (7) + September 2010 (1) + July 2010 (1) + June 2010 (2) + May 2010 (5) + April 2010 (5) + March 2010 (3) + January 2010 (2) * 2009 (31) + October 2009 (1) + September 2009 (1) + August 2009 (3) + July 2009 (2) + June 2009 (4) + May 2009 (6) + April 2009 (2) + March 2009 (4) + January 2009 (8) * 2008 (61) + December 2008 (1) + November 2008 (4) + October 2008 (6) + September 2008 (1) + August 2008 (6) + July 2008 (14) + June 2008 (3) + May 2008 (1) + April 2008 (3) + March 2008 (3) + February 2008 (9) + January 2008 (10) * 2007 (86) + December 2007 (4) + November 2007 (7) + October 2007 (2) + September 2007 (8) + August 2007 (6) + July 2007 (15) + June 2007 (15) + May 2007 (4) + April 2007 (10) + March 2007 (8) + February 2007 (1) + January 2007 (6) * 2006 (103) + December 2006 (4) + November 2006 (3) + October 2006 (7) + September 2006 (6) + August 2006 (13) + July 2006 (4) + June 2006 (13) + May 2006 (7) + April 2006 (9) + March 2006 (6) + February 2006 (13) + January 2006 (18) * 2005 (129) + December 2005 (9) + November 2005 (7) + October 2005 (23) + September 2005 (10) + August 2005 (14) + July 2005 (5) + June 2005 (12) + May 2005 (6) + April 2005 (6) + March 2005 (13) + February 2005 (11) + January 2005 (13) * 2004 (84) + December 2004 (9) + November 2004 (6) + October 2004 (11) + September 2004 (19) + July 2004 (29) + June 2004 (10) Back to top Copyright 2004-2021 Julio Merino [stamp]