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."
optional
thing is very anti-useful because what is the caller supposed to do if it receives nullopt?