Click here to Skip to main content
15,868,141 members
Articles / Programming Languages / C++

Smart Numeric Casts to End the Agony of (int)... or static_cast<int>(...)

Rate me:
Please Sign up or sign in to vote.
4.83/5 (11 votes)
18 Apr 2022CPOL24 min read 10.2K   54   12   27
Smart numeric conversion casts that resolve the issue of should I write (int) or a static_cast by being a better choice than both
Smart numeric conversion casts that are correctly applied using the C style cast syntax, have appropriately verbose and descriptive names and are more strongly typed and resilient to coding errors than a static_cast. In debug builds, exceptions will catch conversion overflows. In release builds, they have zero overhead.

Contents

Introduction

My code is peppered with explicit C style numerical casts, using both standard (int)arg and functional int(arg) syntax and I have been uncomfortable about it for some time. They are all there for a reason. The following patterns are typical:

Example 1

C++
int inum = 5;
double dnum = double(inum) / 2; //result dnum = 2.5

Without the explicit double(inum) cast, the calculation will be done with integer arithmetic resulting in dnum = 2. Using the functional syntax double(inum) that encloses the argument in parenthesis helps here to clarify visually what cast is being applied to.

Example 2

C++
int i= -1;

while (++i < (int) my_std_vector.size())
{

}

The issue here is that my_std_vector.size() returns an unsigned size_t and i is an signed int. Without the cast, I get compiler warnings about the dangers of signed/unsigned mismatch. The explicit cast fixes this problem by converting the unsigned value to signed before the comparison. I choose the standard cast syntax here (int) because I can insert it with one paste and it avoids the accumulation of nested brackets that would occur with the functional syntax, i.e.:

C++
while (++i < int(my_std_vector.size())).  //That terminating ())) already hurts my eyes.

Example 3

C++
double average = sum / n;

int xPixel = (int)  average;

Without the explicit cast, it will compile and do the conversion implicitly but the compiler will issue warnings about possible loss of data in the conversion. The explicit cast shuts up the compiler warnings and replaces them with explicit statements of conversion in my code.

That all makes sense, so why do I still feel uncomfortable about it?

I feel a cultural pressure to replace all those old fashioned C style casts with a proper modern C++ static_cast. I have never been convinced that this will achieve anything other than reduce the readability of my code but I still feel a bit shabby just slapping in (int) in so many places.

I smelled superstition in the advice to use a static_cast in this context but lacked a firm justification for my rejection of it. Feeling a bit too exposed to censure by the language police and haunted by the possibility that I could indeed be missing a trick I decided to devote some time to:

  1. Investigate more deeply just what is going on with these casts and what the dangers are
  2. See if I can find a way of doing it that addresses those potential dangers

The result is the development of a set of smart casts that I present here. They do not need the application of a static_cast because they are themselves already more tightly scoped. They are correctly applied using the C style cast syntax in both standard and function forms. The diagram below shows how they can be applied for conversions between size_t, int and double:

Image 1

They provide a more fine grained choice than normal casting. Each one is defined, scoped and descriptive of a specific action rather than just its destination. They require no special application other than using them in the correct context. e.g.

C++
int i = (to_signed) a_size_t;

int i = (round_to_int) a_double;

double result = to_real(an_int) / 2;

They will not compile if used in the wrong context:

C++
int i = (to_signed) a_double; //ERROR – not a to_signed operation

The Background and Design sections that follow are lengthy but important to demonstrate that this has firm foundations. If you don't have time to read them now, you can skip straight to the Using the Code section.

Background

Two Categories of Casting

It takes some time to read and digest the documentation on casting and although it is clear and unambiguous, it can be difficult to see the overall pattern. I found that it has greater coherency if you start with the distinction between a value cast and a pointer or reference cast. What follows is a narrative review of casting based on that fundamental distinction.

We can say that casting falls into two fundamental categories:

value casting

(new_type) arg or new_type(arg)

and pointer or reference casting

C++
(new_type*) pointer_arg //explicit pointer casting

(new_type&) value_arg //pointer casting with value semantics

The distinction is clearly visible in the functional form of the C style cast for a value cast

C++
new_type(arg) //value cast == construction of temporary new_type object from arg

It is undistinguishable from a call to construct a new_type from arg and that is unequivocally what it is. It won't be interpreted as anything else. If new_type doesn't define a constructor that takes the type of arg or something arg will implicitly convert to, then it won't compile. Otherwise, you will get a new properly constructed new_type object initialized from arg as defined by new_type. It will always be the execution of a defined conversion.

With pointer or reference casting, the functional form cannot even be directly formed:

C++
new_type* (arg) //will not compile, you have to write (new_type*)arg

new_type& (arg) //will not compile, you have to write (new_type&)arg

Pointer and reference casts are unequivocally not calls to construct a new object. They give you a pointer or reference to the original object but interpreted as being of a different type.

Explicit pointer or reference casts are usually written to breach the type system and force an interpretation that would otherwise not be allowed. There are good reasons for doing this, for example:

  • To create an interface between types that have an identical binary representation but are semantically unrelated.
  • To get out of a tight coding corner - it may not be economic to refactor your code to avoid the mismatch that requires it.
  • To perform a clever optimization.

They are always dangerous and require those dangers to be mitigated by how you write and maintain your code.

Not the Same Thing at All

Image 2

Value casts and pointer or reference casts use the same syntactical form but they are two completely different things that have completely different actions. I have not seen this acknowledged emphatically enough in any documentation or literature that I have found and I think that leads to a conflagration of the two in our minds – particularly with the assessment of dangers and risks. Unfortunately, the established terms: cast, conversion and type cast are used interchangeably for both types which further blurs the distinction.

However, with this distinction made, we can now look at the rules for each category. We will start with pointer or reference casts because that is where all the trouble lies.

The Rules for Pointer or Reference Casts

(new_type*) pointer_arg – is an instruction to see pointer_arg as a new_type* in whatever way is possible. new_type doesn't have to be related to the type of pointer_arg in any way at all.

In C++, we also have static_cast, reinterpret_cast and const_cast. They are called in turn by the C style cast but can be specified explicitly .

  • static_cast<new_type*>(pointer_arg) - will look for a hierarchical relationship between the two types and if there isn't one, then it will fail. Otherwise, it will perform the cast and make any pointer adjustments that the hierarchical relationship implies. It is typically used to downcast which is still dangerous.
  • reinterpret_cast<new_type*>(pointer_arg) – is the 'couldn't care less' cast. It doesn't even look for any relationship between the two types . It just blindly reinterprets pointer_arg as being a new_type*. It it typically used to cast between types that are binary compatible, but are semantically unrelated. Being less restrictive than static_cast, it is generally seen as an indication of a greater level of danger.
  • const_cast<new_type*>(pointer_to_new_type) – this can be used to cast away or cast in constness. The danger here is that it can make a const value mutable, which undermines its design.

It is not a good idea to express a pointer or reference cast using the C style casting syntax.

C++
(new_type*) pointer_arg
  • First, a pointer or reference cast is a big deal. It breaks the contract you have with the type system and leaves you exposed to dangers. That needs to be flagged up more verbosely.
  • Secondly, the manner in which it chooses to invoke static_cast, reinterpret_cast and const_cast can have some twists that might surprise you.

Only you know what you want from your pointer cast, so you should say explicitly what it is.

C++
static_cast<new_type*>(pointer_arg) //usually a down cast

reinterpret_cast<new_type*>(pointer_arg) //a blind reinterpretation

const_cast <new_type*>(pointer_to_new_type) //make mutable or const

Don't forget though that even having followed this advice, it is still dangerous.

The one pointer cast that is safe is dynamic_cast which only works with polymorphic types, but can check that a downcast is being made safely.

The Rules for Value Casts

Now let us look at the very different rules that apply to value casting:

new_type(arg) is an unambiguous instruction to construct a new_type from arg and the rules are those defined by the constructors of new_type.

In general and for all user defined types, the C++ static_cast, reinterpret_cast and const_cast have no meaningful role in value casting.

  • reinterpret_cast<new_type>(arg) simply won't compile (there is one exception described below) - value cast are not reinterpretations
  • const_cast<new_type>(arg) won't compile either – that would also be a reinterpretation
  • static_cast<new_type>(arg) will be invoked as new_type(arg) the same unambiguous instruction to construct a new_type.

Value conversions are thoroughly compliant with type safety and memory integrity and threaten neither. The only danger is that you may not get the value you expect – more about that below.

Inbuilt Numerical Types

Now for the value casting of inbuilt numerical types. I will just look at the three that I use. They are:

  • int (signed integers)
  • size_t (the unsigned int that crosses my path)
  • double (floating point and very large numbers)

They all carry implicit conversions from each other. Each has a different nature and they are usually categorized as promotions or narrowing conversion. I would also add 'brittle' as an extra qualifier.

  • Promotion means that the new type can always accurately represent the old type.
  • A narrowing conversion means the new type may be unable to represent the full precision of the old type.
  • Brittle means the new_type may be a wrong interpretation of the old type.

(int) applied to size_t – no change is made, the same bit pattern is interpreted as signed. This is neither a promotion nor a narrowing conversion but it is brittle – if the size_t exceeds a bit more than 2 billion, then the int will come out as negative.

(size_t) applied to int – like the above, no change is made and the same bit pattern is interpreted as unsigned. Again, it is brittle because a negative int will come out as a size_t of a bit more than 2 billion.

(double) applied to int – reformatting is required to convert an int to a double, but it can always be done with no loss of precision. It is a promotion.

(int) applied to double – also requires reformatting but is a narrowing conversion because an int cannot represent the fractional part of a double. It is also brittle because the double could hold a number whose integer part is larger than an int can represent.

I don't use floats but (float) applied to a double would be narrowing, but not brittle.

All of these conversions are implicit requiring no explicit casting operation if the context requires it.

The Slight Twist Exclusive to Built in Integer Types

The built-in integer types (int, unsigned int, and their shorter and longer equivalents) can be converted to and from a pointer of any type but this one must be explicit.

C++
(int) any_pointer

and:

C++
(any_pointer_type)int_arg

This in itself doesn't break the scheme of things. You can get a user defined class to do this by providing it with constructor that takes any pointer and a conversion operator that yields any pointer.

C++
class my_type
{
public:
    template<class T>
    explicit my_type(T*const pT)
    {
        //initialize from this pointer
    }

    template<class T>
    explicit operator T*()
    {
        //return a pointer
    }
}

This will allow:

C++
(my_type)any_pointer

and:

C++
(any_pointer)my_type

The exclusive twist is that not only does the int cast have to be explicit but it is defined as a reinterpret_cast. This is a bit odd because in a sense it is a static_cast. It applies a conversion that has been defined specifically for those types. However, it is a conversion that throws away type information which has to be correctly restored later and this carries the dangers usually associated with reinterpret_cast. It is used classically to store pointers in Windows registers which are then retrieved during event handling. In this scenario, reinterpret_cast flags up the level of danger more appropriately than a static_cast would and I think that is why it was done this way. For this reason, you should always write it as an explicit reinterpret_cast:

C++
reinterpret_cast<int>(any_pointer)

and:

C++
reinterpret_cast<any_pointer_type>(int_arg)

The rules have been twisted slightly so you can do it.

It means built in integers are the only value casts where there is a distinction between the C style casts and the C++ static_cast and reinterpret_cast.

  • (int)arg - will convert from any numeric type and also any pointer type
  • static_cast<int>(arg) – will convert from any numeric type only
  • reinterpret_cast<int>(arg) - will convert from any pointer type only

This little peculiarity does provide an argument for using static_cast when doing integer value casts. It protects you from accidentally converting a pointer to an integer, but it is a marginal benefit.

Design

Requirements

Returning to my dilemma. Should I change all my numerical value conversions from C style (int)arg to static_cast<int>(arg) ?

I think not. These conversions don't merit it, They don't need to be flagged as system threats like pointer or reference casts do and an explicit static_cast will only visually obscure their true identity as conversion constructors. Also, verbose C++ casts are not going to work as danger signs if you habitually use them when there is no danger.

Nevertheless, there are some issues:

  • There is this issue that I could accidentally convert a pointer with (int)arg. I am not likely to make that mistake because I have stuck firmly to prefixing pointers with a p (even if that leads to camel case), but I would prefer to have that risk covered. It is disastrous if it occurs.
  • (int) is a bit brief and it looks exactly the same whether it is applied to a size_t or a double even though the context, operations, losses and risks involved are completely different.
  • Some conversions can go wrong (the brittle ones) and because these are built-in conversions, I can neither step through them in debug builds nor insert run-time health checks to catch when things go wrong.
  • The apparently harmless innovation of adding an & to your cast can wreck everything.

    C++
    double dVal = 12345.6789;
    
    int iVal1 = (int)dVal; //sets iVal1 to 12345
    
    int iVal2 = (int&)dVal; //sets iVal2 to -432932703

Using a static_cast only helps with the first (fairly marginal) issue.

All of these problems can be solved by casting instead to a specially designed intermediate types which, for simplicity, I am calling smart casts.

The Foundation

If we look at the line below from the second of my examples in the introduction:

C++
while (++i < (int) my_std_vector.size())    //why is the (int) there?

The (int) cast there is convert an unsigned int to a signed int. I want a cast specific to this context that says that this is what it is doing. I am going to call it to_signed so I can write:

C++
while (++i < (to_signed) my_std_vector.size())  //that looks better

This is the minimum it needs to work.

C++
class to_signed
{
    const size_t v;
public:
    explicit inline to_signed(size_t _v) : v(_v)    {
    }
    
    inline operator int() const    {
        // you can do checks here
        return static_cast<const int>(v); //the conversion happens here
    }
};

The explicit constructor:

C++
explicit inline to_signed(size_t _v) 

allows it to be converted from a size_t. But only explicitly, i.e., (to_signed) arg or to_signed(arg) and the implicit conversion operator:

C++
inline operator int() const

means that it can always and only be read implicitly as an int.

Image 3

The overall effect is that (to_signed) will convert a size_t to an int just as (int) does. Yes, it is semantically more complex inside, involving multiple calls and copies and indeed in debug builds, you will get this and can step through it all. However, in release builds, it will optimize away to absolutely nothing. That is to say it is zero overhead.

Locking Out Unwanted Behaviour

All we have gained so far is a better name for the cast in this context, the ability to step through the conversion in debug builds and a place where we can put some debug checks. What remains is to make sure that it can't be used out of context. Although we have only provided one constructor that takes a size_t, as it stands it will also take any other built in numeric type because they will all covert implicitly to size_t.

C++
(to_signed) a_double //we don't want this

(to_signed) another_int //we don't want this either

This happens because to_signed having just one constructor makes it clear to the compiler that size_t should be the target of those implicit conversions. We can stop this by providing multiple viable constructors (one more is enough) – it deprives the compiler of a clear target for implicit conversions, so it doesn't try them. So we put in one more constructor to a built in numeric type but make it private because we still only want the constructor taking size_t to work. It will be considered in overload resolution even though ultimately it can't be reached. This may seem strange but it is defined behaviour, you can rely on it.

As a final touch, we delete the default copy constructor. The intended use doesn't require any copies and making it non-copyable will go a long way towards stopping it from being used out of its proper context.

Putting It Together

Putting this together and inserting a debug build numerical health check, we get:

C++
class to_signed
{
    const size_t v;

    inline to_signed(int _v) : v(_v) {
    }

    inline to_signed( to_signed const& _v) = delete; //no copying

public:
    explicit inline to_signed(size_t _v) : v(_v)    {
    }
    
    //This is where the conversion happens
    inline operator const int() const    {
        #ifdef _DEBUG
        if (static_cast<const int>(v) < 0) throw std::out_of_range
                ("unsigned int to large to convert to signed");
        #endif
        return static_cast<const int>(v);
    }
};

The conversion and the debug checks are placed in the conversion operator to avoid throwing an exception during construction. You may want your program to recover, log the error and the failure of an operation and continue with other tasks.

There is a debug check because unsigned int to signed int is a fragile conversion as described above. The easiest way to see if the unsigned int is too large to covert to an int is to cast it to an int (which is this case is costless, even in debug builds) and see if it comes out negative.

Completing the Family

Now I want to define some more smart casts to deal with the other conversions I use that can't be described as to_signed. So I am going to refactor the code to sit on a common base class that will serve for all of them. It comes out like this:

C++
template <class FromType, class To_Type>
class _smart_cast_base
{
protected:
    const FromType v;
    inline _smart_cast_base(_smart_cast_base const& _v) = delete;

    explicit inline _smart_cast_base(To_Type _v) : v(_v)
    {}
public:
    explicit inline _smart_cast_base(FromType _v) : v(_v)
    {}
};

class to_signed : public _smart_cast_base<size_t, int>
{
public:
    using _smart_cast_base::_smart_cast_base;
    
    inline operator const int() const {
#ifdef _DEBUG
        if (static_cast<const int>(v) < 0)
            throw std::overflow_error(
                "unsigned int too large to convert to signed");
#endif
        return static_cast<const int>(v);
    }
};

Personally, my comfort zone is signed integers, but it is now easy to implement a smart cast to go the other way:

C++
class to_unsigned : public _smart_cast_base<int, size_t>
{
public:
    using _smart_cast_base::_smart_cast_base;
        
    inline operator const size_t() const {
#ifdef _DEBUG
        if (v < 0)
            throw std::overflow_error(
                "initialization by negative integer");
#endif
        return static_cast<const size_t>(v);
    }
};

What I do need though is something to replace (int) when I am converting from a double. So what to call it? The built in conversion (int) applied to a double simply discards the fractional part of the double, so it truncates it. That needs to be flagged up and may not always be want you really want, so we will call it (trunc_to_int).

C++
class trunc_to_int : public _smart_cast_base<double, int>
{
public:
    using _smart_cast_base::_smart_cast_base;

    inline operator const int() const {
#ifdef _DEBUG
        if (v > INT_MAX || v < -INT_MAX)
            throw std::overflow_error(
                "double too large to convert to int");
#endif
        return static_cast<const int>(v);
    }
};

Again, this conversion is fragile. A double can hold an integer value that an int can't represent. So we can insert a debug check for that.

Now that we are being more explicit about the built in conversion being a truncation, we might want to reconsider if that is really what we always want. After all, truncating 1.99999 to 1 can throw things out by a factor of two. Unless our concern is how many times something will fit in, we would probably prefer that it be rounded to the nearest integer. So here is a smart cast that does just that called round_to_int:

C++
class round_to_int : public _smart_cast_base<double, int>
{
public:
    using _smart_cast_base::_smart_cast_base;

    inline operator const int() const {
#ifdef _DEBUG
        if (v > INT_MAX || v < -INT_MAX)
            throw std::overflow_error(
                "double too large to convert to int");
#endif
        return static_cast<const int>((v < 0) ? v - 0.5 : v + 0.5);
    }
};

To convert from a double directly to size_t, we provide similar smart casts that also check the transition from signed to unsigned and flag it in their names: trunc_to_unsigned and round_to_unsigned.

C++
class trunc_to_unsigned : public _smart_cast_base<double, size_t>
{
public:
    using _smart_cast_base::_smart_cast_base;

    inline operator const size_t() const {
#ifdef _DEBUG
        if (v > SIZE_MAX || v + 0.5 < 0)
            throw std::overflow_error(
                "double is out of range of size_t");
#endif
        return static_cast<const size_t>(v);
    }
};
class round_to_unsigned : public _smart_cast_base<double, size_t>
{
public:
    using _smart_cast_base::_smart_cast_base;

    inline operator const size_t() const {
#ifdef _DEBUG
        if (v > SIZE_MAX || v + 0.5 < 0)
            throw std::overflow_error(
                "double is out of range of size_t");
#endif
        return static_cast<const size_t>(v + 0.5);
    }
};

Finally, there is the issue of promoting an int to a double. This one is different. It is a promotion, therefore there is nothing that can go wrong. So do we really need to replace the built in (double) cast?

There are a couple of things:

  • I would like it to be more descriptive of what it does instead of just “I want a double”.
  • All of these smart casts will stand marking the boundaries between the use of different numerical types. I don't want them appearing when no conversion needs to be made because that will be misleading and I want them to mark the boundaries between signed and unsigned as well as those between integers and real numbers.

So we have (to_real) which converts an int to a double and (unsigned_to_real) which converts a size_t to a double:

C++
class to_real : public _smart_cast_base<int, double>
{
public:
    using _smart_cast_base::_smart_cast_base;

    inline operator const double() const {
        return static_cast<const double>(v);
    }
};

class unsigned_to_real : public _smart_cast_base<size_t, double>
{
public:
    using _smart_cast_base::_smart_cast_base;

    inline operator const double() const {
        return static_cast<const double>(v);
    }
};

They can't go wrong, so they have no debug checks.

Resilience to the Catastrophic Ampersand

At this point, I decided to revisit the catastrophic ampersand issue mentioned at the beginning of this section:

C++
double dVal = 12345.6789;
int iVal1 = (int)dVal;  //sets iVal1 to 12345
int iVal2 = (int&)dVal; //sets iVal2 to -432932703

That ampersand could get there because someone thought it would be more optimal or simply because it seems the more 'modern' thing to do and once there, it sits there looking so innocent . What it gives you is a pointer cast that sits with value semantics. As there is no hierarchical relationship, it goes straight for reinterpret_cast and iVal2 ends up with the contents of the first 4 encoded bytes of the 8 byte double.

I was expecting these smart casts to be vulnerable to the same fate but to my surprise:

C++
int iVal3 = (trunc_to_int)dVal;  //sets iVal1 to 12345
int iVal4 = (trunc_to_int&)dVal; //sets iVal1 to 12345

So I put in breakpoints to investigate and stepped through the whole process of:

C++
int iVal4 = (trunc_to_int&)dVal;

As expected, no trunc_to_int object gets created and no constructor gets called but the trunc_to_int conversion operator gets called, finds the data value correctly initialized and performs the conversion correctly. How so?

The data value is correctly initialized because being the first and only member and of the same type as the input value, it lines up perfectly with it. That leaves reinterpret_cast looking in the right place for it. The conversion operator gets called because the value semantics cause iVal4 to see it as a trunc_to_int object (even thought no trunc_to_int was created) and calls its implicit conversion operator. This happens because trunc_to_int is never the final destination of the cast. That implicit conversion operator has to be called for it to yield an int.

So these smart_casts are invulnerable to the catastrophic ampersand. In fact, and I hesitate to say this, it produces a slight optimization. It won't make any difference to release builds because the whole process optimizes away to nothing anyway but in debug builds, it skips creating a trunc_to_int object and calling its constructor. I am not going to recommend putting that ampersand there though because if you do it anywhere else, it will be catastrophic. It only works with these smart casts because:

  • They are intermediates, not the final destination.
  • They have just one data value that is of the same type as the input argument.
  • They do the conversion in their conversion operator.
  • The conversion operator is the only way they can be read.

With this, all of the issues that I was concerned about have been addressed. So here, they are applied to the examples in the Introduction section.

Examples of Use

Example 1

C++
int inum = 5;
double dnum = to_real(inum) / 2; //result dnum = 2.5

Example 2

C++
int i= -1;

while (++i < (to_signed) my_std_vector.size())
{

}

Example 3

C++
double average = sum / n;

int xPixel = (round_to_int)  average; //Yes – given the choice, I prefer to round it.

Design Summary

I am happy with this:

  • The smart casts shout loudly enough that they are there and what they are involved in without making the code look ugly or hard to read.
  • Their names describe the conversion process being done and they cannot be used in a context that doesn't fit that description.
  • Their names also flag up the boundaries between signed & unsigned and integer & real.
  • They can check for numerical overflows in debug builds.
  • They are robust, even surviving the catastrophic ampersand.
  • No-one can shout that you should use a static_cast because these smart casts are demonstrably much better for this purpose.

Using the Code

All of the smart casts are defined in smart_numerical_casts.h.

They resolve the issue of should you write (int) arg or static_cast<int>(arg) by being better than both. They are more verbose and descriptive than (int) but equally convenient and embody compile time, run-time and debugging safety features far more attuned to the context than static_cast<int>(arg). In release builds, they optimize to no more than the built-in cast that you would have had to call anyway. They are zero overhead.

Simply use them in place of (int), (double) and (size_t) wherever their name fits the context. Like the built in casts, they can be written in C style standard or functional syntactical forms with the same meaning.

C++
(smart_cast) arg //standard syntax

//or

smart_cast(arg) //functional syntax

This is useful because you can choose the one that sits most comfortably in your code. This flexibility enables you to

  • avoid the unsightly accumulation of nested parenthesis,
  • insert the cast as a prefix with one paste,
  • or delineate clearly where the argument begins and ends.

Don't static_cast them.

C++
static_cast<smart_cast>(arg) //Don't do this!

They are already safer, more descriptive and more fine grained than a static_cast. Save static_cast and reinterpret_cast for pointer and reference casts. This will help to highlight the distinction between value casts (the invocation of defined conversions) and pointer/reference casts (gaming the type system) in your code.

Each one is specific about what it will convert from as well as what it will convert to.

They are:

In place of (int)

  • from a size_t -
    (to_signed) size_t_val

  • from a double -

    (trunc_to_int) double_val<br />
    	(round_to_int) double_val

In place of (size_t)

  • from an int -
    (to_unsigned) int_val

  • from a double - -

    (trunc_to_unsigned)  double_val<br />
    	(round_to_unsigned) double_val

In place of (double)

  • from an int -
    (to_real) int_val

  • from a size_t -
    (unsigned_to_real) size_t_val

They can only be used in their correct context so they will always correctly describe the conversion taking place, loudly proclaiming transitions between signed & unsigned and integer & real. That can be quite an orientation aide when reading your code.

Where a conversion has a potential to produce incorrect results (e.g., signed to unsigned), debug builds will check that numbers do not exceed the boundaries that cause this and throw a run - time exception if they do.

These smart cast are dedicated value casts and always operate as defined conversions.
They will not work as pointer casts:

C++
double dVal = 12345.6789;
int* piVal2 = (trunc_to_int*)&dVal; //error cannot convert trunc_to_int* to int*

and if you use them as reference casts, they will still convert correctly as if it was a value cast - you will have to read the Design section to see why that happens.

C++
double dVal = 12345.6789;
int iVal4 = (trunc_to_int&)dVal;  //correctly sets iVal4 to 12345

This is a unique feature. Normally, that single ampersand would have catastrophic consequences.

But it will not allow you to take a reference to run with:

C++
double dVal = 12345.6789;
int& rVAl = (trunc_to_int&)dVal;  //error cannot convert trunc_to_int to int&

If you use these smart casts in place of (int) (size_t) and (double), then you will find that they will help to orientate you when you read your code.

  • (to_signed) arg - Tells me that arg is unsigned and I am preparing it to participate in signed arithmetic or comparisons. It also tells me I am not expecting arg to have a value anywhere near 2 billion.
  • (trunc_to_int) arg - Tells me that arg is a real number and I am either forming a count of how many times something will fit or I don't want to pay for the overhead of rounding – maybe the numbers are large enough that truncation doesn't matter.
  • (round_to_int) arg - Tells me that arg is a real number and I do really want the best integer approximation. I will see these clustered around my code that sets pixels on the screen that represent the results of calculations carried out with real numbers .
  • (to_real) arg - Tells me that arg is an integer and I want to do real number arithmetic with it.

If I get to use (to_unsigned), (trunc_to_unsigned), (round_to_unsigned) or (unsigned_to_real), then they will serve to remind me of something special about the circumstances that required them. Certainly, any involvement in unsigned arithmetic is best flagged up by them because unsigned arithmetic requires very special attention..

These smart casts are semantically distinct from the built in casts and more fine grained in that they are defined and tightly scoped by operation rather than by destination. They tell you exactly what they are going to do and which numerical representation boundaries they are going to cross.

Further Numeric Smart Casts

int, size_t and double have been taken here to be the default representations of the categories: signed integer, unsigned integer and real number. I rarely use anything else to represent numbers.

It is possible that you may work with types that represent these categories with different storage sizes that may be bigger or smaller, e.g., long long, unsigned char, long double. It is quite easy to create new smart casts to manage this – see the Design section. The hardest thing is deciding what to name them.

Those provided here are named to describe the transitions they make across two mathematical boundaries: signed/unsigned and real numbers/integers. If each of those fields demarcated can have multiple storage sizes, then you have a large pantheon of possibilities each requiring a distinct and descriptive name most of which will never be needed in practice. So they should only be created as needed.

  • If you use them, then you must have a reason for it and that may better inform your imagination as to what to call them.
  • If you work in a uniformly high precision coding environment, you may even want to take long long, unsigned long long and long double as your 'standard' storage sizes so they have the privilege of the least cluttered names.

Names should say what they do and it should be effortless and comfortable to see them say it unless they represent something that truly deserves to be seen as uncomfortable. My decision to write the verbose `unsigned` in full when naming these smart cast represents a degree of discomfort about the transitions they represent. I don't expect to see them often and I want them to shout loudly when I do.

Point of Interest

I guess these smart casts are really just implicitly called non-copyable functors. I can imagine there being other uses for that.

History

  • 18th April, 2002: First publication

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Retired
Spain Spain
Software Author with engineering, science and mathematical background.

Many years using C++ to develop responsive visualisations of fine grained dynamic information largely in the fields of public transport and supply logistics. Currently interested in what can be done to make the use of C++ cleaner, safer, and more comfortable.

Comments and Discussions

 
GeneralMy vote of 1 Pin
YDaoust9-May-22 5:44
YDaoust9-May-22 5:44 
GeneralRe: My vote of 1 Pin
john morrison leon9-May-22 6:00
john morrison leon9-May-22 6:00 
GeneralMy vote of 5 Pin
Michael Sydney Balloni22-Apr-22 8:12
professionalMichael Sydney Balloni22-Apr-22 8:12 
GeneralRe: My vote of 5 Pin
john morrison leon22-Apr-22 13:49
john morrison leon22-Apr-22 13:49 
Generalnew - number_cast<To, From> Pin
john morrison leon9-May-22 1:18
john morrison leon9-May-22 1:18 
QuestionNice Writeup + no dynamic_cast? Pin
David On Life19-Apr-22 12:02
David On Life19-Apr-22 12:02 
AnswerRe: Nice Writeup + no dynamic_cast? Pin
john morrison leon19-Apr-22 12:32
john morrison leon19-Apr-22 12:32 
GeneralRe: Nice Writeup + no dynamic_cast? Pin
David On Life22-Apr-22 7:58
David On Life22-Apr-22 7:58 
AnswerRe: Nice Writeup + no dynamic_cast? Pin
Michael Sydney Balloni22-Apr-22 7:28
professionalMichael Sydney Balloni22-Apr-22 7:28 
GeneralRe: Nice Writeup + no dynamic_cast? Pin
David On Life22-Apr-22 7:57
David On Life22-Apr-22 7:57 
GeneralRe: Nice Writeup + no dynamic_cast? Pin
john morrison leon22-Apr-22 13:49
john morrison leon22-Apr-22 13:49 
Answernew - number_cast<To, From> Pin
john morrison leon9-May-22 1:19
john morrison leon9-May-22 1:19 
GeneralSkeptical about the benefits Pin
YDaoust19-Apr-22 0:00
YDaoust19-Apr-22 0:00 
GeneralRe: Skeptical about the benefits Pin
john morrison leon19-Apr-22 2:18
john morrison leon19-Apr-22 2:18 
Generalnew - number_cast<To, From> Pin
john morrison leon9-May-22 1:21
john morrison leon9-May-22 1:21 
GeneralRe: new - number_cast<To, From> Pin
YDaoust9-May-22 1:31
YDaoust9-May-22 1:31 
GeneralRe: new - number_cast<To, From> Pin
john morrison leon9-May-22 1:48
john morrison leon9-May-22 1:48 
GeneralRe: new - number_cast<To, From> Pin
YDaoust9-May-22 2:31
YDaoust9-May-22 2:31 
GeneralRe: new - number_cast<To, From> Pin
john morrison leon9-May-22 5:38
john morrison leon9-May-22 5:38 
QuestionWhat about 64 bits size_t ? Pin
JMH_FR18-Apr-22 23:45
professionalJMH_FR18-Apr-22 23:45 
AnswerRe: What about 64 bits size_t ? Pin
john morrison leon19-Apr-22 1:35
john morrison leon19-Apr-22 1:35 
AnswerRe: What about 64 bits size_t ? Pin
john morrison leon19-Apr-22 4:17
john morrison leon19-Apr-22 4:17 
Answernew - number_cast<To, From> Pin
john morrison leon9-May-22 1:19
john morrison leon9-May-22 1:19 
QuestionSafeInt Library Pin
Randor 18-Apr-22 11:17
professional Randor 18-Apr-22 11:17 
AnswerRe: SafeInt Library Pin
john morrison leon18-Apr-22 14:16
john morrison leon18-Apr-22 14:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.