Previous section   Next section

Imperfect C++ Practical Solutions for Real-Life Programming
By Matthew Wilson
Table of Contents
Chapter 29.  Arithmetic Types


29.10. Truncations, Promotions, and Tests

The story so far looks pretty spanking good, methinks. However, we're not there yet. Even though we've discussed two possible mechanisms for retrieving the underlying values from large integer types when we need them, there're several operations of the built-in integer types that we're yet to address.

29.10.1 Truncations

Although we usually couch mention of truncation in negative terms, there are occasions when it is precisely what is required. Since the built-in integral types support truncation, we should try to provide it for our large integer types. For a 64-bit type, we'd naturally want to be able to truncate to 32-bit and smaller types. The only way we can facilitate this as part of the natural syntax is to provide an implicit conversion operator:



class SInteger64


{


  . . .


  operator sint32_t () const


  . . .



However, there's a very nasty problem here. If we provide this operator then, aside from the usual problems of implicit conversion, we will be facilitating a truncation without warnings. Even though it's implementation defined, compilers do provide truncation warnings for fundamental types, and we certainly want the same from our large integral types.

Given that neither of our two classes are templates, the truncation inside these operators will be detected at the epoch of compilation of the functions, rather than of their use. If the operators are inline, all client code of these types will be informed, in each client compilation unit, that the classes are flawed, which will serve only to disincline people from using them. If the operators are in separate implementation files, then no one's going to see the warnings anyway.

Another, slightly better, option is to use explicit casts (see section 19.5). Since we're talking about returning fundamental types by value, explicit casts will work perfectly well:



class SInteger64


{


  . . .


  operator explicit_cast<sint32_t>() const


  . . .


};





SInteger64  i64;


sint32_t    i32 = explicit_cast<sint32_t>(i64);



The problem here is that once again the client code will not receive a warning about the truncation. It's a little better than with the implicit conversion operator, since at least there's something eye-catching in the source, but I still don't think it's good enough.

The only solution I can think of is to copy the explicit cast mechanism to a new template—called a truncation_cast—that will work in the same way as explicit cast, but will provide a more obvious sign of the truncation by virtue of its name.

But I really think this is a step too far from sanity, so I'd content myself to have methods to perform the truncations:



class UInteger64


{


  . . .


  uint32_t truncate32() const;


  . . .



Note that there's still no truncation warning by the compiler, but that's not needed because we won't be using the large integer types in the same way as built-in types syntactically.

29.10.2 Promotions

As well as truncating values, sometimes we may also wish to promote the values to other numeric types, specifically float, double and long double. The obvious way to do this is to provide implicit conversion operators:



class SInteger64


{


  . . .


  operator float() const;


  operator double() const;


  operator long double() const;


  . . .



Unfortunately, this blows all our previous arithmetic operators out of the water. An expression such as the following now fails due to a conflict between the nonmember addition operator and the inbuilt arithmetic operator.



SInteger64  i1;


SInteger64  i2;


i1 = i2 + 100;



This is because the expression can be interpreted to mean either convert 100 to SInteger64 and then add it to i2, or convert i2 to float (or double, or long double) and add it to the 100.

We cannot provide an implicit conversion to any numeric type, or we lose all our arithmetic operators. The only answer is to have explicit casts or conversion methods.

29.10.3 Tests

The last aspect of built-in integer operations is the ability to implicitly take part in Boolean conditional subexpressions. Even though I don't like to use such things for integers (see section 17.2.1), it is still a part of built-in integral type semantics, and so we should consider it for our large integer types.

We've just learned that any integral type cannot be used for our Boolean operator, so we'd need to use either the void*(), T *(), or int T::*() options we covered in Chapter 24. Naturally the best thing would be to use the "proper" Boolean generator macro.

Alas, the addition statement we've just seen would also cause ambiguities. It could be interpreted to mean either convert 100 to SInteger64 and then add it to i2, or convert i2 to int operator_bool_generator< SInteger64>::* and then increment that pointer by 100.

It sure makes you weep sometimes, doesn't it?

The answer here is to go back to our trusty friend the attribute shim (see section 20.2), and declare is_true() and is_not() shims:



inline bool is_true(SInteger64 const &i)


{


  return i != 0;


}


inline bool is_not(SInteger64 const &i) // or is_not_true()


{


  return i == 0;


}



This can then be used in the largely digestible form:



SInteger64 i = . . .





if(is_true(i))


{


  . . .



In my opinion, though, this is just one more nail in the coffin for the use of non-Boolean expressions and their implicit interpretation to Boolean, so if you stick to the advice in section 17.2.1 you'll never have to worry about such things.



if(0 != i) // What could be simpler?


{


  . . .




      Previous section   Next section