I have a class, and its interface looks like this:
struct cRect;
class Serializer;
struct cPoint
{
public:
//I named these 'value_type' in preparation of templating the type, but I haven't templated it yet.
typedef int value_type;
value_type x;
value_type y;
public:
cPoint() : x(0), y(0) { }
cPoint(value_type x, value_type y) : x(x), y(y) { }
explicit cPoint(const std::string &str);
cPoint(const cPoint &point);
~cPoint() = default;
//Keeps the point within 'rect'.
void KeepWithin(const cRect &rect);
//Keeps the point outside of 'rect'.
void KeepWithout(const cRect &rect);
//Snaps the point to the nearest edge of 'rect', regardless of
//whether the point is inside or outside the rectangle.
void SnapToEdge(const cRect &rect);
//Returns the absolute distance between this point and 'other'.
unsigned DistanceFrom(const cPoint &other);
//Returns true if we are within 'distance' of 'other'. This is faster than 'DistanceFrom', because it saves a sqrt().
bool WithinDistanceOf(const cPoint &other, unsigned distance) const;
//Returns 'true' if 'value.x' is greater than 'min.x' and less than 'max.x', and the same for the 'y' member.
static bool IsWithin(const cPoint &min, const cPoint &value, const cPoint &max);
static bool IsWithin(const cPoint &value, const cPoint &max);
//Keeps 'value.x' between 'min.x' and 'max.x', with 'loops.x' being the number of times it had to loop around.
//Does the same with the 'y' member. Negative loops return a negative number.
static cPoint LoopInRange(const cPoint &min, const cPoint &value, const cPoint &max, cPoint &loops);
static cPoint LoopInRange(const cPoint &value, const cPoint &max, cPoint &loops);
bool operator==(const cPoint &other) const;
bool operator!=(const cPoint &other) const;
cPoint &operator=(const cPoint &other); //Assignment operator
cPoint &operator+=(const cPoint &other);
cPoint &operator-=(const cPoint &other);
cPoint &operator*=(const cPoint &other);
cPoint &operator/=(const cPoint &other);
cPoint &operator%=(const cPoint &other);
cPoint operator+(const cPoint &other) const;
cPoint operator-(const cPoint &other) const;
cPoint operator*(const cPoint &other) const;
cPoint operator/(const cPoint &other) const;
cPoint operator%(const cPoint &other) const;
//Additive-inverse operator.
cPoint operator-() const;
//Packs the cPoint into a Uint32, with 16 bits for x, and 16 for y. Since both x and y can be negative,
//this leaves just 15 bits for the numeral component, meaning (-32768 to 32768) in both x and y.
uint32_t ToUint32() const;
void FromUint32(uint32_t data);
//Format: "(x, y)"
std::string ToString() const;
void FromString(const std::string &str);
void Serialize(Serializer &serializer);
SFML_ONLY
(
sf::Vector2f ToSfmlVector2f() const;
void FromSfmlVector2f(sf::Vector2f vector);
)
QT_ONLY
(
QPoint ToQPoint() const;
void FromQPoint(QPoint point);
)
};
(Note: the 'c' in 'cPoint' doesn't stand for 'class' - it's to indicate it's part of my 'Common' code base, and so is a namespace-like prefix, but since the class is so commonly used, I don't want to always call it 'Common::Point' like I do for some of my other classes)
I have similar classes called cSize, and cRect. I'm happy with the current usability of all three (point, size, and rect). However, I find I have a need to compress the size of these classes down in rare circumstances.
I'd like to have a cPoint16 and a cSize16 class - where both the x and y member of cPoint16 would be an int16_t so the entire class would fit into four bytes instead of eight. I understand there'll be some tiny hit to performance, but performance isn't yet a bottleneck in this area, but RAM looks like it might soon become one.
To prevent redundant code, I was thinking about making cPoint templated on the integer type of the x and y components, so I'd probably go:
typedef Common::Point<int32_t> cPoint;
typedef Common::Point<int16_t> cPoint16;
This would also allow me the (likely to be called upon) future flexibility of: Common::Point<float>
I'd like this possible cPoint16 class to be compatible in comparison with the regular cPoint.
That is, I'd like to be able to do things like: cPoint16 == cPoint, cPoint += cPoint16, and etc...
This does open me up to potential int overflows, but that's something that's also true with normal uint16_t / uint32_t operations, and is something one naturally has in the back of their mind when they are directly working with variables of differing sizes - and with the cPoint16 naming, I think it'd be fairly apparent to me and probably won't be much of an issue in practice (I could even use asserts to validate results to check for overflows when in debug mode).
Because of this interoperability, cPoint operations might actually have to be templated as well.
This:
cPoint &operator+=(const cPoint &other);
Might need to become something like this:
template<typename OtherPointType>
cPoint &operator+=(const OtherPointType &other);
Which opens me up to potential problems of 'OtherPointType' not even having to be remotely the same type, as long as it has an 'x' and a 'y' member, which would be weird.
I could staticly assert that ThisPointType and OtherPointType both have integer or both have floating point values, and block mixing and matching of floating point and integers, while still allowing mixing and matching of various sizes of integers or sizes of floats.
If I wanted to semi-ensure both point types are actually a Common::Point templated class, I enable_if() the operators based on the existence of some private non-functional function or typedef, or something like that.
It'd be easy to just say that cPoint and cPoint16 shouldn't be compatible with each other, but I think their compatibility would be very convenient and, while not necessary, something that would be used alot - bearing in mind that cPoint16 is not compact for serialization, but compact for active/persistent usage when a hundred thousand structs are active. The cPoint16 instances would almost always be added to regular cPoints.
There's also the precedent of interoperability of ints that are different sizes - as much as I might suggest to myself to never mix operations of different size ints, that's a good guideline most of the time, but there are frequent situations where they eventually do need to be added together with the final result.
Anyway, I'm interested in hearing your code design thoughts on this matter.