Improved solution
I tried to improve xDELx's solution and came up with this. You can see the whole project here.
Checked.hpp
This is the core of the project. This does the validation of the values and throwing of exceptions if needed.
A few important notes here:
- I wanted this class to seamlessly stand in for numeric types. Hence the usage of template argument, cast operator and assignment operator overloading.
- Because of the usage of templates, the class cannot be separated to header (.h) and source (.cpp) file, everything has to be kept in a single file (hence the extension .hpp).
- I didn't want to have the (somewhat) clumsy get/set operators, so made sure when this class is used, it can be changed, but cannot be reassigned. For this, the assignment and move assignment operators got removed.
- _min and _max do not need to changed, so they are marked constant.
- set() and get() are kept, but they are not necessary if someone wants to see fewer functions, their functionality can be moved into the overloaded operators.
#pragma once
#include "OutOfBoundsException.hpp"
#include "ValueNotSetException.hpp"
#include "ValueTooLargeException.hpp"
template<typename T> class Checked
{
private:
const T _min;
const T _max;
T _actual;
bool _isValid;
public:
Checked(T actual, T min, T max) :
_min(min),
_max(max)
{
if (min > max)
throw ValueTooLargeException(min, max, "min cannot be larger than max.");
set(actual);
}
Checked & operator=(const Checked & other) = delete; // Do not allow assignment
Checked & operator=(Checked && other) = delete; // Do not allow move assignment
bool isValid() const { return _isValid; }
T getMin() const { return _min; }
T getMax() const { return _max; }
T get() const { return _isValid ? _actual : throw ValueNotSetException("no valid value have been provided."); }
void set(T value)
{
if (_min <= value && value <= _max)
{
_actual = value;
_isValid = true;
}
else
{
throw OutOfBoundsException(value, _min, _max, "trying to set an invalid value.");
_isValid = false;
}
}
Checked & operator=(T value)
{
set(value);
return *this;
}
operator T() const { return get(); }
};
Avatar
After introduction of the Checked class, the Avatar becomes really simple. One just needs to define the attributes and the Checked class takes care of their behaviour.
Avatar.h
#pragma once
#include "Checked.h"
class Avatar
{
private:
const std::string _name;
static std::string visualiseAttribute(int value, int min, int max);
public:
Checked<int> strength;
Checked<int> speed;
Checked<int> agility;
Avatar(std::string name, int strength, int speed, int agility);
void display();
};
Avatar.cpp
#include "Avatar.h"
#include <iostream>
#include <sstream>
Avatar::Avatar(std::string name, int strength, int speed, int agility) :
_name(std::move(name)),
strength(strength, 1, 100),
speed(speed, 1, 100),
agility(agility, 1, 100)
{
}
void Avatar::display()
{
std::cout << "Avatar: " << _name << std::endl;
std::cout << "\tStrength: " << strength << ' ' << visualiseAttribute(strength, 1, 100) << std::endl;
std::cout << "\tSpeed: " << speed << ' ' << visualiseAttribute(speed, 1, 100) << std::endl;
std::cout << "\tAgility: " << agility << ' ' << visualiseAttribute(agility, 1, 100) << std::endl;
}
std::string Avatar::visualiseAttribute(int value, int min, int max)
{
std::stringstream ss;
for (int i = min; i <= max; i++)
ss << (i <= value ? '+' : ' ');
return ss.str();
}
The Exceptions
They are pretty much what one would expect. Inherit from existing one and add some extra info.
OutOfBoundsException.hpp
#pragma once
#include <stdexcept>
#include <sstream>
template<typename T> class OutOfBoundsException : public std::out_of_range
{
private:
static std::string createMessage(T actual, T min, T max, const std::string & message = "")
{
std::stringstream ss;
ss << actual << " is out of bounds [" << min << ", " << max << "]: " << message;
return ss.str();
}
public:
OutOfBoundsException(T actual, T min, T max, const std::string & message = "") :
std::out_of_range(createMessage(actual, min, max, message))
{
}
};
ValueTooLargeException.hpp
#pragma once
#include <stdexcept>
#include <sstream>
template<typename T> class ValueTooLargeException : public std::out_of_range
{
private:
static std::string createMessage(T actual, T max, const std::string & message = "")
{
std::stringstream ss;
ss << actual << " cannot exceed " << max << "]: " << message;
return ss.str();
}
public:
ValueTooLargeException(T actual, T max, const std::string & message = "") :
std::out_of_range(createMessage(actual, max, message))
{
}
};
ValueNotSetException.hpp
#pragma once
#include <stdexcept>
class ValueNotSetException : public std::runtime_error
{
public:
ValueNotSetException(const std::string & message = "") :
std::runtime_error(message)
{
}
};
Test The Application
One can test the behaviour with some simple code in the main, such as:
#include "Avatar.h"
#include <iostream>
int main()
{
try
{
Avatar avatar1("Avatar 1", 10, 15, 17);
//avatar1.setStrength(0); // Out of bounds
//avatar1.setSpeed(101); // Out of bounds
//avatar1.agility = Checked<int>(16, 15, 17); // Cannot use deleted function
avatar1.display();
Avatar avatar2("Avatar 2", 5, -3, 8); // Out of bounds in constructor
avatar2.display();
}
catch (OutOfBoundsException<int> & outOfBoundsException)
{
std::cerr << "Exception caught: " << outOfBoundsException.what() << std::endl;
}
return 0;
}