![]() | |
![]() ![]() |
![]() | Imperfect C++ Practical Solutions for Real-Life Programming By Matthew Wilson |
Table of Contents | |
Chapter 29. Arithmetic Types |
29.3. Initialization (Value Construction)The next thing we need to do with our integer type is be able to initialize our type from another value. Given that the type is unsigned, we give it the ability to construct from a single uint32_t, as well as construct from two uint32_t values: class UInteger64 { . . . UInteger64(uint32_t i) // The low 32-bits { this->lowerVal = i; this->upperVal = 0; } UInteger64(uint32_t upper, uint32_t lower); . . . In this case, I've chosen to make it non explicit, so that it is a conversion constructor (C++-98: 12.3.1), because the type is to be a very simple type. We'll see that this seemingly innocuous decision has profound implications for the rest of the class design. Now we can write the following normal looking statement: UInteger64 i = 0; Unfortunately, things get a little tricky now. Since it's a 64-bit integer, it's overwhelmingly likely that we'd want to be able to initialize it from a uint64_t when using the class with a compiler that supports that type. class UInteger64 { . . . UInteger64(uint32_t i); #ifdef ACMELIB_UINT64_T_SUPPORT UInteger64(uint64_t i) { get_union_().i = i; } #endif ACMELIB_UINT64_T_SUPPORT . . . However, now we're in trouble. Specifying an argument of any unsigned type smaller than 64 bits—again, we're assuming that int is 32 bits here—will resolve to the uint32_t overload. Specifying an argument of uint64_t will use that overload. But integer literals that do bear a modifying suffix (see section 15.4.2) are interpreted as int or long or, although it's not part of the standard, as sint64_t where supported. One option is to add a third constructor for int:
class UInteger64
{
. . .
UInteger64(uint32_t i);
#ifdef ACMELIB_UINT64_T_SUPPORT
UInteger64(uint64_t i);
#endif ACMELIB_UINT64_T_SUPPORT
UInteger64(int i);
There are two problems with this. First, now we can pass an int with a negative value. Although we can write the constructor to simply cast it to an unsigned value, this is semantically meaningless—you cannot represent a negative number in an unsigned quantity, merely use it as a repository for the bits to be placed back in a signed variable at a later stage. This factor alone rules out this as a sensible strategy. There's also a practical objection. Say you want to be able to convert from unsigned long, unsigned int, unsigned short, and unsigned char, which is perfectly reasonable (assuming long is not larger than 64 bits), then you'll run into ambiguities. Because uint32_t may be implemented, on our range of compilers alone, as unsigned int, unsigned long or unsigned __int32 (see section 13.2), we will have to do a lot of preprocessor discrimination in order to have a portable class. You end up in a mess like that shown in Listing 29.3. Listing 29.3.class UInteger64 { . . . UInteger64(uint32_t i); #ifdef ACMELIB_UINT64_T_SUPPORT UInteger64(uint64_t i); #endif ACMELIB_UINT64_T_SUPPORT UInteger64(int i); if defined(ACMELIB_COMPILER_IS_BORLAND) || \ . . . CodeWarrior, Comeau, Digital Mars, GCC defined(ACMELIB_COMPILER_IS_WATCOM) UInteger64(unsigned int i); #elif defined(ACMELIB_COMPILER_IS_INTEL) UInteger64(unsigned long i); #elif defined(ACMELIB_COMPILER_IS_MSVC) UInteger64(unsigned long i); # ifdef ACMELIB_32BIT_INT_IS_DISTINCT_TYPE UInteger64(unsigned int i); # endif /* ACMELIB_32BIT_INT_IS_DISTINCT_TYPE */ #endif /* compiler */ The answer is to use conditional compilation to select the best constructor whose type is supported by the current compiler. It's hardly beautiful, but it's unambiguous and a lot prettier than the alternative: class UInteger64 { . . . #ifdef ACMELIB_UINT64_T_SUPPORT UInteger64(uint64_t i); #else /* ? ACMELIB_UINT64_T_SUPPORT */ UInteger64(uint32_t i); #endif ACMELIB_UINT64_T_SUPPORT . . . In fact, you must provide the larger-type constructor, since it is all too easy to experience a truncation with the 32-bit constructor when given arguments of 64 bits. A significant minority of compilers will fail to warn about this, particularly inside template code. Note that there's a small potential efficiency issue here. Even though inside either constructor there is a total of 64 bits being set by the compiler, and that therefore it is highly likely that they have exactly the same costs, in client code the uint64_t version will have to extend any 32-bit (or smaller) arguments to 64 bits. But even if this isn't made moot by compiler optimizations, it's a small price that's well worth paying to avoid all the ambiguities and countless overloads. |
![]() | |
![]() ![]() |