I think, it is easier explain using an example. Let's take a class that model the speed of a Formula 1 car, the interface may look something like:
class SpeedF1
{
public:
explicit SpeedF1(double speed);
double getSpeed() const;
void setSpeed(double newSpeed);
//other stuff as unit
private:
double speed_;
};
Now, negative speed are not meaningful in this particular case and neither value greater then 500 km/h. In this case the constructor and the setSpeed function may throw exceptions if the value provide is not within a logical range.
I can introduce an extra layer of abstraction and insert a extra object instead of double. The new object will be a wrapper around the double and it will be construct and never modify. The interface of the class will be changed to:
class ReasonableSpeed
{
public:
explicit ReasonableSpeed(double speed); //may throw a logic error
double getSpeed() const;
//no setter are provide
private:
double speed_;
};
class SpeedF1
{
public:
explicit SpeedF1(const ReasonableSpeed& speed);
ReasonableSpeed getSpeed() const;
void setSpeed(const ReasonableSpeed& newSpeed);
//other stuff as unit
private:
ReasonableSpeed speed_;
};
With this design SpeedF1 cannot throw, however I need to extra pay an object constructor every time I want to reset the speed.
For class where a limited set of value are reasonable (for example the Months in a calendar) I usually make the constructor private and provide a complete set of static functions. In this case it is impossible, another possibility is implement a null object pattern but I am not sure it is superior to simply throw an exception.
Finally, my question is:
What is the best practise to solve this kind of problem?
You absolutely should throw an exception from a constructor if you're unable to create a valid object. This allows you to provide proper invariants in your class. In practice, you may have to be very careful.
Throwing exceptions from C++ constructors Since C++ constructors do not have a return type, it is not possible to use return codes. Therefore, the best practice is for constructors to throw an exception to signal failure. The throw statement can be used to throw an C++ exception and exit the constructor code.
The short answer is NO. You would throw an exception if the application can't continue executing with the bad data. In your example, the logic is to display an error message on the front end and Option 2 is the cleaner method for achieving this requirement.
Don't use init()/cleanup() members. If you have to call init() every time you create an instance, everything in init() should be in the constructor. The constructor is meant to put the instance into a consistent state which allows any public member to be called with a well-defined behavior.
First off, don’t overestimate the cost of the extra constructor. In fact, this cost should be exactly the cost of initialising a double
plus the cost for the validity check. In other words, it is likely equal to using a raw double
.
Secondly, lose the setter. Setters – and, to a lesser degree, getters – are almost always anti-patterns. If you need to set a new (maximum) speed, chances are you actually want a new car.
Now, about the actual problem: a throwing constructor is completely fine in principle. Don’t write convoluted code to avoid such a construct.
On the other hand, I also like the idea of self-checking types. This makes the best use of C++’ type system and I’m all in favour of that.
Both alternatives have their advantages. Which one is best really depends on the exact situation. In general, I try to exploit the type system and static type checking as much as possible. In your case, this would mean having an extra type for the speed.
I strongly vote for the second option. This is only my personal opinion without a lot of academic backing. My experience is that setting up a "pure" system that operates on only valid data makes your code a lot cleaner. This can be achieved by using your second approach which ensures that only valid data enters the system.
If your system grows, you may find that ReasonableSpeed
gets used in a lot of places (use your discretion, but chances are things actually get reused quite a lot). The second approach will save you a lot of error checking codes in the long term.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With