• Create Account

# Design thoughts for compatible classes with different variable sizes?

Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

9 replies to this topic

### #1Servant of the Lord  Crossbones+   -  Reputation: 12827

Like
0Likes
Like

Posted 17 September 2013 - 01:28 PM

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;

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.

It's perfectly fine to abbreviate my username to 'Servant' rather than copy+pasting it all the time.

All glory be to the Man at the right hand... On David's throne the King will reign, and the Government will rest upon His shoulders. All the earth will see the salvation of God.                                                                                                                                                       [Need free cloud storage? I personally like DropBox]

### #2dmatter  Crossbones+   -  Reputation: 2435

Like
1Likes
Like

Posted 17 September 2013 - 01:38 PM

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.

Why not this:

template <typename OtherT>
Point &operator+=(const Point<OtherT> &other);

Now it has to be a Common::Point.

You might also use enable_if to ensure T and otherT are both integer types (as opposed to integer and float, for example).

Edited by dmatter, 17 September 2013 - 01:39 PM.

### #3Servant of the Lord  Crossbones+   -  Reputation: 12827

Like
0Likes
Like

Posted 17 September 2013 - 01:47 PM

Good solution! I hadn't thought of that.

It's perfectly fine to abbreviate my username to 'Servant' rather than copy+pasting it all the time.

All glory be to the Man at the right hand... On David's throne the King will reign, and the Government will rest upon His shoulders. All the earth will see the salvation of God.                                                                                                                                                       [Need free cloud storage? I personally like DropBox]

### #4RobTheBloke  Crossbones+   -  Reputation: 1948

Like
1Likes
Like

Posted 17 September 2013 - 01:58 PM

template<typename T>
struct Point2

{
template<typename S>
inline Point2<T> operator + (const Point2<S>& b) const
{
return Point2<T>(x + b.x, y + b.y);
}
T x;
T y;
};


\edit

beaten to it.

Edited by RobTheBloke, 17 September 2013 - 01:59 PM.

### #5AllEightUp  Moderators   -  Reputation: 3654

Like
0Likes
Like

Posted 17 September 2013 - 04:48 PM

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.

Why not this:

template <typename OtherT>
Point &operator+=(const Point<OtherT> &other);
Now it has to be a Common::Point.

You might also use enable_if to ensure T and otherT are both integer types (as opposed to integer and float, for example).

Another possible variation would be to remove the operator completely and go with possibly insane generality:

template< typename LhsType, typename RhsType > inline
LhsType& operator +=( LhsType& lhs, Point2< RhsType >& rhs )
{
// Viable version of code here.
}

template<> inline
cPoint16& operator +=( cPoint16& lhs, Point2< float >& rhs );
// Link error if someone tries..

Not really that much different but gives you more options to control acceptable variations and you can place all the desired variations in a single file somewhere.

Edited by AllEightUp, 17 September 2013 - 04:49 PM.

### #6Trienco  Crossbones+   -  Reputation: 1693

Like
1Likes
Like

Posted 19 September 2013 - 10:33 PM

If performance isn't an issue, why not add explicit casts between the classes? All operations are automatically compatible and by requiring explicit casting, the code will always highlight locations where you need to pay attention to type sizes.

f@dzhttp://festini.device-zero.de

### #7Servant of the Lord  Crossbones+   -  Reputation: 12827

Like
0Likes
Like

Posted 24 September 2013 - 02:16 PM

Another question, in the same context: What makes more sense in naming?

If I had (to simplify the question):

struct cPoint<Type>
{
Type x, y;
};

Should cPoint<int16_t> be named cPoint16 because the values it contains are 16 bits, or should it be named cPoint32 because Point as a whole fits into 32 bits?

So a regular cPoint<int32_t> would be named cPoint64 or cPoint32. On the other hand, a cPoint<float> and cPoint<double> would be named cPointF and cPointD (if I ever bothered to define and use them), or should I do cPointF32, cPointF64. Or maybe cPoint32f, or cFPoint32...

What would be your suggested naming scheme?

Also, out of curiosity, when you see something like uint32_t, do you think of those 32 bits in terms of, "this integer can hold a value of up to 2^32" (do you see the 32 bits in terms of the range/precision of the value), or do you think, "this integer fits into 32 bits of memory" (do you see the 32 bits primarily in terms of storage)?

Obviously you are aware of both, but what strikes your mind first?

Normally, I just use 'ints' and 'unsigned' when I need general values, and only start using 'int32_t', 'int16_t', 'uint32_t', etc... when I need (or am at least recommending to the compiler) a specific bit size, usually for storage purposes. However, when I need a larger integer size, I go for uint32_t by habit (forgetting/ignoring unsigned long long), so I guess I'm a bit inconsistent with that!

Edited by Servant of the Lord, 24 September 2013 - 02:18 PM.

It's perfectly fine to abbreviate my username to 'Servant' rather than copy+pasting it all the time.

All glory be to the Man at the right hand... On David's throne the King will reign, and the Government will rest upon His shoulders. All the earth will see the salvation of God.                                                                                                                                                       [Need free cloud storage? I personally like DropBox]

### #8dmatter  Crossbones+   -  Reputation: 2435

Like
0Likes
Like

Posted 24 September 2013 - 02:54 PM

What would be your suggested naming scheme?

I think I'd rather the typedef reflected how the template was parameterised, i.e. cPoint16 for a cPoint<int16_t> and cPointF for cPoint<float>.

One thing to think about is whether you really need to typedef all the variants, you could just typedef the common case and leave the others for explicit template instantiation.

Also, out of curiosity, when you see something like uint32_t, do you think of those 32 bits in terms of, "this integer can hold a value of up to 2^32" (do you see the 32 bits in terms of the range/precision of the value), or do you think, "this integer fits into 32 bits of memory" (do you see the 32 bits primarily in terms of storage)?
Obviously you are aware of both, but what strikes your mind first?

Tough question because, as you say, you have to be aware of both. I suppose since maximum value is bounded by the storage then thinking about the storage first and maximum value second would make the most sense.

Ideally if someone were interested in the maximum value rather than the storage, then they would use the 'fast' types like int_fast32_t. I rarely see them used though.

Edited by dmatter, 24 September 2013 - 02:55 PM.

### #9Servant of the Lord  Crossbones+   -  Reputation: 12827

Like
0Likes
Like

Posted 24 September 2013 - 06:00 PM

I think I'd rather the typedef reflected how the template was parameterised, i.e. cPoint16 for a cPoint<int16_t> and cPointF for cPoint<float>.

That's what I was leaning too myself - it just seemed more intuitive to me, though I'm not sure why.

Logically, since I think of uint16_t as '16 bits' not 'holding 2^16 values', it seems like I should go with cPoint<int16_t> = cPoint32 as '32 bits total', not cPoint16 as 'holding values that 16 bits allow', but it feels off, for some reason. The name is just unintuitive to me.

One thing to think about is whether you really need to typedef all the variants, you could just typedef the common case and leave the others for explicit template instantiation.

Yeah, I'll likely only typedef the ones I use regularly, and have commented-out typedefs that I can uncomment if their use starts to become common (this is just so I can remember what naming scheme I chose for them).

Also, out of curiosity, when you see something like uint32_t, do you think of those 32 bits in terms of, "this integer can hold a value of up to 2^32" (do you see the 32 bits in terms of the range/precision of the value), or do you think, "this integer fits into 32 bits of memory" (do you see the 32 bits primarily in terms of storage)?
Obviously you are aware of both, but what strikes your mind first?

Tough question because, as you say, you have to be aware of both. I suppose since maximum value is bounded by the storage then thinking about the storage first and maximum value second would make the most sense.

Ideally if someone were interested in the maximum value rather than the storage, then they would use the 'fast' types like int_fast32_t. I rarely see them used though.

I don't think I've ever used those, or the int_least32_t.

I'll likely name cPoint<int> as just cPoint (instead of cPoint32), letting it just be whatever makes sense to the compiler.

Thanks for the feedback!

[Edit:] Hadn't noticed the malformatting - fixed it.

It's perfectly fine to abbreviate my username to 'Servant' rather than copy+pasting it all the time.

All glory be to the Man at the right hand... On David's throne the King will reign, and the Government will rest upon His shoulders. All the earth will see the salvation of God.                                                                                                                                                       [Need free cloud storage? I personally like DropBox]

### #10dmatter  Crossbones+   -  Reputation: 2435

Like
1Likes
Like

Posted 25 September 2013 - 05:08 AM

it feels off, for some reason. The name is just unintuitive to me.

I can provide some alternative reasoning for my previously suggested names, see what you think...

Perhaps the reason the naming feels a bit awkward is because the size (number of components) of cPoint is implicitly 2 and the fact that it has integer components is also implicit.

In the past, I've seen names like this:

vec2i
vec2f

Then for unsigned types:

vec2ui
vec2ul

Building on that kind of convention for fixed-width types:

vec2i16
vec2i32

Which can be thought of a as a vector of 2x int16's or int32's respectively. So the overall size is just the mathematical expression as read: 2x16 or 2x32.

So you might call yours:

cPoint2i16
cPoint2i32

Now, if you want to make bits of that name implicit with the cPoint type then I don't see why the other parts of the name should necessary have to change meaning.

Say that cPoint simply is defined as being a 2-vector, we might remove the sizing from the name (replacing with an underscore just for readability):

cPoint_i16
cPoint_i32

And if you want integer types to be default/implied then we can remove that too (and collapse the underscore now) which brings us back to:

cPoint16
cPoint32

The meaning of the numbers hasn't changed at all.

Similarly, with a cPointF, that doesn't mean the entire cPoint fits into a single float, it is just shorthand for (an imaginary) cPoint2F, which itself would just be shorthand for: Common::Point<2, float>

Edited by dmatter, 25 September 2013 - 05:12 AM.

Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

PARTNERS