Let's say I am writing an API, and one of my functions take a parameter that represents a channel, and will only ever be between the values 0 and 15. I could write it like this:
void Func(unsigned char channel) { if(channel < 0 || channel > 15) { // throw some exception } // do something }
Or do I take advantage of C++ being a strongly typed language, and make myself a type:
class CChannel { public: CChannel(unsigned char value) : m_Value(value) { if(channel < 0 || channel > 15) { // throw some exception } } operator unsigned char() { return m_Value; } private: unsigned char m_Value; }
My function now becomes this:
void Func(const CChannel &channel) { // No input checking required // do something }
But is this total overkill? I like the self-documentation and the guarantee it is what it says it is, but is it worth paying the construction and destruction of such an object, let alone all the additional typing? Please let me know your comments and alternatives.
The advantage of strongly typed languages is that the compiler can detect when an object is being sent a message to which it does not respond. This can prevent run-time errors. The other advantages of strong typing are: earlier detection of errors speeds development.
The key differences between using a loosely typed language compared to a strongly typed languages. In programming we call a language loosely typed when you don't have to explicitly specify types of variables and objects. A strongly typed language on the contrary wants types specified.
A strongly typed programming language is one in which each type of data, such as integers, characters, hexadecimals and packed decimals, is predefined as part of the programming language, and all constants or variables defined for a given program must be described with one of the data types.
C++ is reasonably strongly typed, and the ways in which it has been lenient that have historically caused trouble have been pruned back, such as implicit casts from void* to other pointer types, and finer grained control with explicit casting operators and constructors.
If you wanted this simpler approach generalize it so you can get more use out of it, instead of tailor it to a specific thing. Then the question is not "should I make a entire new class for this specific thing?" but "should I use my utilities?"; the latter is always yes. And utilities are always helpful.
So make something like:
template <typename T> void check_range(const T& pX, const T& pMin, const T& pMax) { if (pX < pMin || pX > pMax) throw std::out_of_range("check_range failed"); // or something else }
Now you've already got this nice utility for checking ranges. Your code, even without the channel type, can already be made cleaner by using it. You can go further:
template <typename T, T Min, T Max> class ranged_value { public: typedef T value_type; static const value_type minimum = Min; static const value_type maximum = Max; ranged_value(const value_type& pValue = value_type()) : mValue(pValue) { check_range(mValue, minimum, maximum); } const value_type& value(void) const { return mValue; } // arguably dangerous operator const value_type&(void) const { return mValue; } private: value_type mValue; };
Now you've got a nice utility, and can just do:
typedef ranged_value<unsigned char, 0, 15> channel; void foo(const channel& pChannel);
And it's re-usable in other scenarios. Just stick it all in a "checked_ranges.hpp"
file and use it whenever you need. It's never bad to make abstractions, and having utilities around isn't harmful.
Also, never worry about overhead. Creating a class simply consists of running the same code you would do anyway. Additionally, clean code is to be preferred over anything else; performance is a last concern. Once you're done, then you can get a profiler to measure (not guess) where the slow parts are.
Yes, the idea is worthwhile, but (IMO) writing a complete, separate class for each range of integers is kind of pointless. I've run into enough situations that call for limited range integers that I've written a template for the purpose:
template <class T, T lower, T upper> class bounded { T val; void assure_range(T v) { if ( v < lower || upper <= v) throw std::range_error("Value out of range"); } public: bounded &operator=(T v) { assure_range(v); val = v; return *this; } bounded(T const &v=T()) { assure_range(v); val = v; } operator T() { return val; } };
Using it would be something like:
bounded<unsigned, 0, 16> channel;
Of course, you can get more elaborate than this, but this simple one still handles about 90% of situations pretty well.
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