3

I am trying to explain and understand a way to create object which cannot have "default" instances. Typically I want classes which represent, as closely as possible, the real-life concept I am attempting to model in software. What I mean specifically is, if it doesn't make sense to create a "default" something in the real world, it shouldn't make sense to do so in software either. I will use a Ball as an example for my question.

A simple ball has three "attributes": color, diameter (in inches), and "bounciness" (some factor of how "bouncy" it is).

A straight-forward representation of this simple ball in code would be the following struct. Assume Color, Inches, and Bounciness are trivial types that represent what their names say.

struct Ball
{
    Color color;
    Inches diameter;
    Bounciness bounciness;
};

In my mind, this is the "C" (i.e., non OOP) way of representing a ball. This has a few problems, but the one I want to focus on is the problem of default values. I argue that (at least for this example) it is impossible to have a "default" ball. What would be the default color? What would be the default diameter? It can't be 0 because that's not a valid diameter for a ball. In other words, you can't reasonably create a valid ball with default values. In the struct example, the problem is potentially even worse because, if the fields are primitive types, the default (uninitialized) values could be completely random.

My solution would be to remove that ability to create a default instance of Ball by requiring a constructor that takes all arguments like so:

class Ball
{
public:
    Ball(Color color, Inches diameter, Bounciness bounciness):
        _color(color),
        _diameter(diameter),
        _bounciness(bounciness)
    {}

private:
    Color _color;
    Inches _diameter;
    Bounciness _bounciness;
};

In this way, we are guaranteed to have valid/intended values for each field once an object of Ball is created.

However, if the class becomes more complex, the constructor grows as well and I might end up with something like the following in code:

Ball myBall{colors::kGreen, 30_inches, Bounciness::kExtraBouncy, 10, 100, 1_inches, patterns::kStripes};

As a reminder, I have made it a requirement that there cannot be default values for any of these fields.

Well, I know that there is a design pattern that addresses this issue: the builder pattern. I believe I could write a builder for the above Ball class like so:

class BallBuilder
{
public:
    std::optional<Ball> create()
    {
        if (!_color) return std::nullopt;
        if (!_diameter) return std::nullopt;
        if (!_bounciness) return std::nullopt;

        return Ball{_color.value(), _diameter.value(), _bounciness.value()};    
    }

    void setColor(Color v) { _color = v; }
    void setDiameter(Diameter v) { _diameter= v; }
    void setBounciness(Bounciness v) { _bounciness= v; }

private:
    std::optional<Color> _color;
    std::optional<Inches> _diameter;
    std::optional<Bounciness> _bounciness;
};

I use std::optional to indicate the "state" of each field as it is added to the builder. create will only return a value instance of Ball if all of the fields have been provided to the builder. Thus, instances of Ball always have a valid, complete state for all fields.

I have a problem with this solution however. Because this is C++, the goal is usually to be as efficient as possible. Well, my builder solution above has to maintain copies of of all the fields in the builder AND in the instance of Ball which is created. There are also potentially extra calls to copy constructors, move constructors, and assignment operators.

As a concrete example, assume the following sizes (in RAM) for each of the fields:

Color _color; //4 bytes
Inches _diameter; //4 bytes
Bounciness _bounciness; //8 bytes

When used like:

BallBuilder builder; //> 16 bytes
builder.setColor(colors::kRed);
builder.setDiameter(10_inches);
builder.setBounciness(Bounciness::kExtraBouncy);

auto myBall = builder.create(); //16 bytes

Greater than 32 bytes are used plus calls to copy, move, and assignment operators.

Whereas

Ball myBall{colors::kRed, 10_inches, Bounciness::kExtraBouncy} myBall;

would only use 16 bytes and a single call to the non-default constructor.

In understand that this might just be the tradeoff of more "correct" code, but I am wondering if there is any other option(s) that improves memory efficiency while still providing this level of "safety."

8
  • 6
    "Classes should represent, as closely as possible, the "real-life" concept they are attempting to model." Why do you believe this? I disagree with this and suspect this philosophy will lead toward over-engineering. Classes are for modeling and organizing abstractions; you only need to model to the degree required to accomplish the automation that is the purpose of the program.
    – Erik Eidt
    Commented Apr 10, 2023 at 17:28
  • 4
    You say no defaults. I say no matter what you do any idiot can still write myDefaultBallBuilder() that makes 10 meter chrome balls that don’t bounce. Commented Apr 10, 2023 at 19:32
  • 4
    What's your issue with the constructor based approach? It seems pretty reasonable to me
    – Alexander
    Commented Apr 11, 2023 at 2:35
  • 3
    Unless you're writing on an extremely limited embedded platforms, your efficiency concerns over a few bytes of stack space are misplaced. Commented Apr 11, 2023 at 7:39
  • 1
    This optional thing is very anti-useful because what is the caller supposed to do if it receives nullopt?
    – user20574
    Commented Apr 11, 2023 at 18:41

3 Answers 3

3

In this way, we are guaranteed to have valid/intended values for each field once an object of Ball is created.

Stating that there can be no default ball does not inherently limit the existence of a default color value, amount of inches, or bounciness. How do you intend to guard against those defaults?

However, if the class becomes more complex, the constructor grows as well and I might end up with [a constructor with lots of parameters)

Well, I know that there is a design pattern that addresses this issue: the builder pattern.

No, the builder pattern is not the correct solution here. While this is technically not part of the definition of the builder pattern, a builder tends not to guarantee that every single value is provided. It specifically separates the individual value setters so that a consumer can set the values that they are interested in.

To put it differently: in your builder example snippet, if I were to remove builder.setBounciness(Bounciness::kExtraBouncy);, would the compiler throw an error? No.

Therefore, even if you were to write explicit validation that the builder refuses to build unless everything has been checked it would be (a) obfuscated from the developer when writing the code and (b) pass by the compiler and throw a runtime error (I'm aware you're returning null and not throwing an exception, but you're signing up for a lifetime of null checks and run the risk of runtime null reference exceptions, which is just as bad as throwing an explicit builder exception)

I also don't get your point. You dislike a long list of constructor values, but you are actively replacing it with an equally long list of chained methods which then still list every value anyway. That's inherently longer, you're effectively turning 1,2,3 into .SetFoo(1).SetBar(2).SetBaz(3).

Update in hindsight:
if the issue is that the parameters are unnamed and it's difficult to understand which value maps to which property, then a builder might be a good fix for that, however it still infringes on your initial requirement of forcing all values to be set, and if this is an immovable requirement for you, I maintain that the builder is not the right pattern for you here.

An explicit constructor is the better solution here. It is the only way to force that every requested value is passed into the construction of the object, it tells the developer right away that their code is invalid, and will throw a compilation error instead of a runtime error.

Pedantically, a factory is an equal solution here as it can force the same thing, but it does not provide more benefit than an explicit constructor in this regard, and it's therefore unwarranted (unless you have ulterior reasons that warrant a factory).

3
  • 1
    I disagree that Builder is inappropriate here. Using Builder as a workaround for missing named parameters is wholly appropriate and very common in many languages, including C++, Java and Rust. Commented Apr 11, 2023 at 7:38
  • 1
    @SebastianRedl If the goal is to make explicit (intendedly) that no default values are allowed, the builder pattern is a horrible solution, basically because it obfuscates that info. It's rather the opposite. The builder pattern is "good" in absence of optional, named parameters with default values. Build pattern "ease" the construction by giving no specific order and dependency among the arguments. Which is not the case described by the OP. IMO.
    – Laiv
    Commented Apr 11, 2023 at 11:34
  • 1
    @SebastianRedl: I agree with your core point. A builder is a great solution to clearly name your parameters. However, OP has also stated adamantly that they want to force every value to be explicitly set. Compared to a constructor, the builder pattern removes that enforcement. Based on how insistent the OP was about that behavior, I inferred that they'd not be willing to make that tradeoff, which is why I stated that the builder pattern might not be right for them.
    – Flater
    Commented Apr 11, 2023 at 23:05
1

Sometimes I don’t care. I just want any ball. There comes the default constructor. Plus you have constructors with all arguments having default values, giving you “any ball”, “any red ball”, “any red 10 inch ball” or “any red ten inch leather ball”.

1
  • Yes, I suppose in some cases you could have defaults. However, Ball was just a simple example (I want to apply this to other cases) and I did state the requirement that there cannot by defaults. Commented Apr 10, 2023 at 17:10
1

I think explicit constructor parameters is a perfectly valid solution to the ‘no defaults’ requirement.

If you’re concerned about too many parameters, consider breaking your class into smaller classes which mode the characteristics - in your Ball example: material, behaviour, etc - and then have the Ball class use Composition.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.