EXPLORATION 25

image

Big and Little Numbers

Another common use for overloading is to write functions that work just as well with large and small integers as with plain integers. C++ has five different integer types, ranging in size from 8 bits to 64 bits or larger, with several choices for sizes in between. This Exploration takes a look at the details.

The Long and Short of It

The size of an int is the natural size of an integer on the host platform. For your desktop computer, that probably means 32 bits or 64 bits. Not too long ago, it meant 16 bits or 32 bits. I’ve also used computers with 36-bit and 60-bit integers. In the realm of desktop computers and workstations, 32-bit and 64-bit processors dominate today’s computing landscape, but don’t forget specialized devices, such as digital signal processors (DSPs) and other embedded chips, where 16-bit architectures are still common. The purpose of leaving the standard flexible is to ensure maximum performance for your code. The C++ standard guarantees that an int can represent, at a minimum, any number in the range –32,767 to 32,767, inclusive.

Although your desktop computer most likely uses two’s complement representation for integers, C++ does not mandate that format, only that the representation be binary. In other words, you should treat an integer as a number, not a bit pattern. (Wait for Exploration 64, if you need to work at the bit level.)

To discover the number of bits in an integer, use std::numeric_limits, as you did way back in Listing 2-3. Try that same program, but substitute int forbool . What do you get for the output? ________________

Most likely, you got 31, although some of you may have seen 15 or 63. The reason for this is that digits does not count the sign bit. No matter what representation your computer uses for an integer, one of those bits must indicate whether the number is negative or positive. Thus, for a type that represents a signed quantity, such as int, you must add one to digits, and for a type with no sign, such as bool, use digits without further modification. Fortunately, std::numeric_limits offers is_signed, which is true for a signed type and false for a type without a sign bit. Rewrite Listing 2-3 to use is_signed to determine whether to add one to digits and print the number of bits per int and perbool .

Check your answers. Are they correct? ________________ Compare your program with Listing 25-1.

Listing 25-1.  Discovering the Number of Bits in an Integer

#include <iostream>
#include <limits>
 
int main()
{
  std::cout << "bits per int = ";
  if (std::numeric_limits<int>::is_signed)
    std::cout << std::numeric_limits<int>::digits + 1 << ' ';
  else
    std::cout << std::numeric_limits<int>::digits << ' ';
 
  std::cout << "bits per bool = ";
  if (std::numeric_limits<bool>::is_signed)
    std::cout << std::numeric_limits<bool>::digits + 1 << ' ';
  else
    std::cout << std::numeric_limits<bool>::digits << ' ';
}

Long Integers

Sometimes, you need more bits than int can handle. In this case, add long to the definition to get a long integer.

long int lots_o_bits{2147483647};

You can even drop the int, as shown in the following:

long lots_o_bits{2147483647};

The standard guarantees that a long int can handle numbers in the range –2,147,483,647 to 2,147,483,647, but an implementation can choose a large size. C++ does not guarantee that a long int is actually longer than a plain int. On some platforms, int might be 32 bits and long might be 64. When I first used a PC at home, an int was 16 bits and long was 32 bits. At times, I’ve used systems for which int and long were both 32 bits. I’m writing this book on a machine that uses 32 bits for int and 64 bits for long.

The type long long int can be even bigger, with a range of at least –9,223,372,036,854,775,807 to 9,223,372,036,854,775,807. You can drop the int, if you wish, and programmers often do.

Use long long if you want to store numbers as large as possible and are willing to pay a small performance penalty (on some systems, or a large penalty on others). Use long if you have to ensure portability and must represent numbers outside the range ±32,767 (which is all C++ guarantees for type int).

Short Integers

Sometimes, you don’t have the full range of an int, and reducing memory consumption is more important. In this case, use a short int, or just short, which has a guaranteed range of at least –32,767 to 32,767, inclusive. This is the same guaranteed range as int, but implementations often choose to make an int larger than the minimum and keep short in this range of 16 bits. But you must not assume that int is always larger than short. Both may be the exact same size.

As is done with long, you define a type as short int or short.

short int answer{42};
short zero{0};

Modify Listing 25-1 to print the number of bits in a long and a short too. How many bits are in a long on your system? ______________ How many in a short? ________________ long long? ___________

When I run the program in Listing 25-2, I get 16 bits in a short, 32 in an int, and 64 in a long and long long. On another computer in my network, I get 16 bits in a short, 32 in an int and long, and 64 in a long long.

Listing 25-2.  Revealing the Number of Bits in Short and Long Integers

#include <iostream>
#include <limits>
 
int main()
{
  std::cout << "bits per int = ";
  if (std::numeric_limits<int>::is_signed)
    std::cout << std::numeric_limits<int>::digits + 1 << ' ';
  else
    std::cout << std::numeric_limits<int>::digits << ' ';
 
  std::cout << "bits per bool = ";
  if (std::numeric_limits<bool>::is_signed)
    std::cout << std::numeric_limits<bool>::digits + 1 << ' ';
  else
    std::cout << std::numeric_limits<bool>::digits << ' ';
 
  std::cout << "bits per short int = ";
  if (std::numeric_limits<short>::is_signed)
    std::cout << std::numeric_limits<short>::digits + 1 << ' ';
  else
    std::cout << std::numeric_limits<short>::digits << ' ';
 
  std::cout << "bits per long int = ";
  if (std::numeric_limits<long>::is_signed)
    std::cout << std::numeric_limits<long>::digits + 1 << ' ';
  else
    std::cout << std::numeric_limits<long>::digits << ' ';
 
  std::cout << "bits per long long int = ";
  if (std::numeric_limits<long long>::is_signed)
    std::cout << std::numeric_limits<long long>::digits + 1 << ' ';
  else
    std::cout << std::numeric_limits<long long>::digits << ' ';
}

Integer Literals

When you write an integer literal (that is, an integer constant), the type depends on its value. If the value fits in an int, the type is int; otherwise, the type is long or long long. You can force a literal to have type long by adding l or L (the letter L in lowercase or uppercase) after the digits. (Curiously, C++ has no way for you to type a short literal.) I always use uppercase L because a lowercase l looks too much like the digit 1. The compiler can always tell the difference, but every year it gets a little harder for me to see the difference between 1 and l. Use two consecutive L’s for a long long.

Devise a way for a program to print int=, followed by the value, for anint literal; printlong= , followed by the value, for along literal; and printlong long= , followed by the value, for along long literal. (Hint: What was the topic of the previous Exploration?) Write a program to demonstrate your idea, and test it with some literals. If you can, run the program on platforms that use different sizes for int and long. Compare your program to that of Listing 25-3.

Listing 25-3.  Using Overloading to Distinguish Types of Integer Literals

#include <iostream>
#include <locale>
 
void print(int value)
{
  std::cout << "int=" << value << ' ';
}
 
void print(long value)
{
  std::cout << "long=" << value << ' ';
}
 
void print(long long value)
{
  std::cout << "long long=" << value << ' ';
}
 
int main()
{
  std::cout.imbue(std::locale{""});
  print(0);
  print(0L);
  print(32768);
  print(-32768);
  print(2147483647);
  print(-2147483647);
  print(2147483648);
 
  print(9223372036854775807);
  print(-9223372036854775807);
}

The actual types that your compiler chooses will vary. Your compiler may even be able to handle larger integer literals. The C++ standard sets down some guaranteed ranges for each of the integer types, so all the values in Listing 25-3 will work with a decent C++ 11 compiler. If you stick to the guaranteed ranges, your program will compile and run everywhere; outside the range, you’re taking your chances. Library-writers have to be especially careful. You never know when someone working on a small, embedded processor might like your code and want to use it.

Byte-Sized Integers

The smallest integer type that C++ offers is signed char. The type name looks similar to the character type, char, but the type acts differently. It usually acts like an integer. By definition, the size of signed char is 1 byte, which is the smallest size that your C++ compiler supports for any type. The guaranteed range of signed char is –127 to 127.

In spite of the name, you should try not to think of signed char as a mutated character type; instead, think of it as a misspelled integer type. Many programs have a typedef similar to

typedef signed char byte;

to make it easier for you to think of this type as a byte-sized integer type.

There is no easy way to write a signed char literal, just as there is no way to write a simple short literal. Character literals have type char, not signed char. Besides, some characters may be out of range for signed char.

Although the compiler does its best to help you remember that signed char is not a char, the standard library is less helpful. The I/O stream types treat signed char values as characters. Somehow, you have to inform the stream that you want to print an integer, not a character. You also need a solution to create signed char (and short) literals. Fortunately, the same solution lets you use signed char constants and print signed char numbers: type casting.

Type Casting

Although you cannot write a short or arbitrary signed char literal directly, you can write a constant expression that has type short or signed char and take any suitable value. The trick is to use a plain int and tell the compiler exactly what type you want.

static_cast<signed char>(-1)
static_cast<short int>(42)

The expression does not have to be a literal, as demonstrated in the following:

int x{42};
static_cast<short>(x);

The static_cast expression is known as a type cast. The operator, static_cast, is a reserved keyword. It converts an expression from one type to another. The “static” in its name means the type is static, or fixed, at compile time.

You can convert any integer type to any other integer type. If the value is out of range for the target type, you get junk as a result. For example, the high-order bits may be discarded. Thus, you should always be careful when using static_cast. Be absolutely sure that you are not discarding important information.

If you cast a number to bool, the result is false if the number is zero or true if the number is not zero (just like the conversion that takes place when you use an integer as a condition).

Rewrite Listing 25-3 to overload print for short and signed char values too. Use type casting to force various values to different types and ensure that the results match your expectations. Take a look at Listing 25-4 to see one possible solution.

Listing 25-4.  Using Type Casts

#include <iostream>
#include <locale>
 
typedef signed char byte;
 
void print(byte value)
{
  // The << operator treats signed char as a mutant char, and tries to
  // print a character. In order to print the value as an integer, you
  // must cast it to an integer type.
  std::cout << "byte=" << static_cast<int>(value) << ' ';
}
 
void print(short value)
{
  std::cout << "short=" << value << ' ';
}
 
void print(int value)
{
  std::cout << "int=" << value << ' ';
}
 
void print(long value)
{
  std::cout << "long=" << value << ' ';
}
 
void print(long long value)
{
  std::cout << "long long=" << value << ' ';
}
 
int main()
{
  std::cout.imbue(std::locale{""});
  print(0);
  print(0L);
  print(static_cast<short>(0));
  print(static_cast<byte>(0));
  print(static_cast<byte>(255));
  print(static_cast<short>(65535));
  print(32768);
  print(32768L);
  print(-32768);
  print(2147483647);
  print(-2147483647);
  print(2147483648);
  print(9223372036854775807);
  print(-9223372036854775807);
}

When I run Listing 25-4, I get -1 for static_cast<short>(65535) and static_cast<byte>(255). That’s because the values are out of range for the target types. The resulting value is mere coincidence. In this case, it is related to the bit patterns that my particular compiler and platform happen to use. Different environments will yield different values.

Make Up Your Own Literals

Although C++ does not offer a built-in way to create a short literal, you can define your own literal suffix. Just as 42L has type long, you can invent a suffix, say, _S, to mean short, so 42_S is a compile-time constant of type short. Listing 25-5 shows how you can define your own literal suffix.

Listing 25-5.  User-Defined Literal

#include <iostream>
 
short operator "" _S(unsigned long long value)
{
    return static_cast<short>(value);
}
 
void print(short s)
{
   std::cout << "short=" << s << ' ';
}
 
void print(int i)
{
   std::cout << "int=" << i << ' ';
}
 
int main()
{
   print(42);
   print(42_S);
}

When the user defines a literal, it is known as a user-defined literal, or UDL. The name of the literal must begin with an underscore. This will let the C++ standard define additional literals that don’t start with an underscore without fear of interfering with the literals you define. You can define a UDL for integer, floating-point, and string types.

Integer Arithmetic

When you use signed char and short values or objects in an expression, the compiler always turns them into type int. It then performs the arithmetic or whatever operation you want to do. This is known as type promotion. The compiler promotes a short to an int. The result of arithmetic operations is also an int.

You can mix int and long in the same expressions. C++ converts the smaller type to match the larger type, and the larger type is the type of the result. This is known as type conversion, which is different from type promotion. (The distinction may seem arbitrary or trivial, but it’s important. The next section will explain one of the reasons.) Remember: Promotesigned char and short to int; convertint to long.

long big{2147483640};
short small{7};
std::cout << big + small; // promote small to type int; then convert it to long;
                          // the sum has type long

When you compare two integers, the same promotion and conversion occurs: the smaller argument is promoted or converted to the size of the larger argument. The result is always bool.

The compiler can convert any numeric value to bool; it considers this a conversion on the same level as any other integer conversion.

Overload Resolution

The two-step type conversion process may puzzle you. It matters when you have a set of overloaded functions, and the compiler has to decide which function to call. The first thing the compiler tries is to find an exact match. If it can’t find one, it searches for a match after type promotion. Only if that fails does it search for a match allowing type conversion. Thus, it considers a match based only on type promotion to be better than type conversion. Listing 25-6 demonstrates the difference.

Listing 25-6.  Overloading Prefers Type Promotion over Type Conversion

#include <iostream>
 
// print is overloaded for signed char, short, int and long
void print(signed char value)
{
  std::cout << "print(signed char = " << static_cast<int>(value) << ") ";
}
 
void print(short value)
{
  std::cout << "print(short = " << value << ") ";
}
 
void print(int value)
{
  std::cout << "print(int = " << value << ") ";
}
 
void print(long value)
{
  std::cout << "print(long = " << value << ") ";
}
 
// guess is overloaded for bool, int, and long
void guess(bool value)
{
  std::cout << "guess(bool = " << value << ") ";
}
 
void guess(int value)
{
  std::cout << "guess(int = " << value << ") ";
}
 
void guess(long value)
{
  std::cout << "guess(long = " << value << ") ";
}
 
// error is overloaded for bool and long
void error(bool value)
{
  std::cout << "error(bool = " << value << ") ";
}
 
void error(long value)
{
  std::cout << "error(long = " << value << ") ";
}
 
int main()
{
  signed char byte{10};
  short shrt{20};
  int i{30};
  long lng{40};
 
  print(byte);
  print(shrt);
  print(i);
  print(lng);
 
  guess(byte);
  guess(shrt);
  guess(i);
  guess(lng);
 
  error(byte); // expected error
  error(shrt); // expected error
  error(i);    // expected error
  error(lng);
}

The first four lines of main call the print function. The compiler always finds an exact match and is happy. The next four lines call guess. When called with signed char and short arguments, the compiler promotes the arguments to int and finds an exact match with guess(int i).

The last four lines call the aptly named function, error. The problem is that the compiler promotes signed char and short to int and then must convert int to either long or bool. It treats all conversions equally, thus it cannot decide which function to call, so it reports an error. Delete the three lines that I marked with “expected error,” and the program works just fine, or add an overload for error(int value), and everything will work.

The problem of ambiguous overload resolution is a difficult hurdle for new C++ programmers. It’s also a difficult hurdle for many experienced C++ programmers. The exact rules for how C++ resolves overloaded names are complicated and subtle and will be covered in depth in Exploration 69. Avoid being clever about overloaded functions, and keep it simple. Most overloading situations are straightforward, but if you find yourself writing an overload for type long, be certain you also have an overload for type int.

Knowing about big integers helps with some programs, but others have to represent even larger numbers. The next Exploration examines how C++ works with floating point values.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset