Team LiB   Previous Section   Next Section

2.5 Type Declarations

One of the hallmarks of C++ is that you can define a type that resembles any built-in type. Thus, if you need to define a type that supports arbitrary-sized integers—call it bigint—you can do so, and programmers will be able to use bigint objects the same way they use int objects.

You can define a brand new type by defining a class (see Chapter 6) or an enumeration (see Section 2.5.2 later in this chapter). In addition to declaring and defining new types, you can declare a typedef, which is a synonym for an existing type. Note that while the name typedef seems to be a shorthand for "type definition," it is actually a type declaration. (See Section 2.5.4 later in this chapter.)

2.5.1 Fundamental Types

This section lists the fundamental type specifiers that are built into the C++ language. For types that require multiple keywords (e.g., unsigned long int), you can mix the keywords in any order, but the order shown in the following list is the conventional order. If a type specifier requires multiple words, one of which is int, the int can be omitted. If a type is signed, the signed keyword can be omitted (except in the case of signed char).

bool

Represents a Boolean or logical value. It has two possible values: true and false.

figs/acorn.gifchar

Represents a narrow character. Narrow character literals usually have type char. (If a narrow character literal contains multiple characters, the type is int.) Unlike the other integral types, a plain char is not necessarily equivalent to signed char. Instead, char can be equivalent to signed char or unsigned char, depending on the implementation. Regardless of the equivalence, the plain char type is distinct and separate from signed char and unsigned char.

All the narrow character types (char, signed char, and unsigned char) share a common size and representation. By definition, char is the smallest fundamental type—that is, sizeof(char) is 1.

double

Represents a double-precision, floating-point number. The range and precision are at least as much as those of float. A floating-point literal has type double unless you use the F or L suffix.

float

Represents a single-precision, floating-point number.

long double

Represents an extended-precision, floating-point number. The range and precision are at least as much as those of double.

signed char

Represents a signed byte.

signed int

Represents an integer in a size and format that is natural for the host environment.

signed long int

Represents an integer whose range is at least as large as that of int.

signed short int

Represents an integer such that the range of an int is at least as large as the range of a short.

unsigned char

Represents an unsigned byte. Some functions, especially in the C library, require characters and character strings to be cast to unsigned char instead of plain char. (See Chapter 13 for details.)

unsigned long int

Represents an unsigned long integer.

unsigned short int

Represents an unsigned short integer.

void

Represents the absence of any values. You cannot declare an object of type void, but you can declare a function that "returns" void (that is, does not return a value), or declare a pointer to void, which can be used as a generic pointer. (See static_cast<> under Section 3.5.2 for information about casting to and from a pointer to void.)

wchar_t

Represents a wide character. Its representation must match one of the fundamental integer types. Wide character literals have type wchar_t.

figs/acorn.gif

The representations of the fundamental types are implementation-defined. The integral types (bool, char, wchar_t, int, etc.) each require a binary representation: signed-magnitude, ones' complement, or two's complement. Some types have alignment restrictions, which are also implementation-defined. (Note that new expressions always return pointers that are aligned for any type.)

The unsigned types always use arithmetic modulo 2n, in which n is the number of bits in the type. Unsigned types take up the same amount of space and have the same alignment requirements as their signed companion types. Nonnegative signed values are always a subset of the values supported by the equivalent unsigned type and must have the same bit representations as their corresponding unsigned values.

Although the size and range of the fundamental types is implementation-defined, the C++ standard mandates minimum requirements for these types. These requirements are specified in <climits> (for the integral types) and <cfloat> (for the floating-point types). See also <limits>, which declares templates for obtaining the numerical properties of each fundamental type.

2.5.2 Enumerated Types

An enumerated type declares an optional type name (the enumeration) and a set of zero or more identifiers (enumerators) that can be used as values of the type. Each enumerator is a constant whose type is the enumeration. For example:

enum logical { no, maybe, yes };
logical is_permitted = maybe;

enum color { red=1, green, blue=4 };
const color yellow = static_cast<color>(red | green);

enum zeroes { a, b = 0, c = 0 };

You can optionally specify the integral value of an enumerator after an equal sign (=). The value must be a constant of integral or enumerated type. The default value of the first enumerator is 0. The default value for any other enumerator is one more than the value of its predecessor (regardless of whether that value was explicitly specified). In a single enumeration declaration, you can have more than one enumerator with the same value.

The name of an enumeration is optional, but without a name you cannot declare use the enumeration in other declarations. Such a declaration is sometimes used to declare integer constants such as the following:

enum { init_size = 100 };
std::vector<int> data(init_size);

figs/acorn.gif

An enumerated type is a unique type. Each enumerated value has a corresponding integer value, and the enumerated value can be promoted automatically to its integer equivalent, but integers cannot be implicitly converted to an enumerated type. Instead, you can use static_cast<> to cast an integer to an enumeration or cast a value from one enumeration to a different enumeration. (See Chapter 3 for details.)

The range of values for an enumeration is defined by the smallest and largest bitfields that can hold all of its enumerators. In more precise terms, let the largest and smallest values of the enumerated type be vmin and vmax. The largest enumerator is emax and the smallest is emin. Using two's complement representation (the most common integer format), vmax is the smallest 2n - 1, such that vmax max(abs( emin) - 1, abs( emax)). If emin is not negative, vmin = 0; otherwise, vmin = -( vmax + 1).

figs/acorn.gif

In other words, the range of values for an enumerated type can be larger than the range of enumerator values, but the exact range depends on the representation of integers on the host platform, so it is implementation-defined. All values between the largest and smallest enumerators are always valid, even if they do not have corresponding enumerators.

In the following example, the enumeration sign has the range (in two's complement) -2 to 1. Your program might not assign any meaning to static_cast<sign>(-2), but it is semantically valid in a program:

enum sign { neg=-1, zero=0, pos=1 };

figs/acorn.gif

Each enumeration has an underlying integral type that can store all of the enumerator values. The actual underlying type is implementation-defined, so the size of an enumerated type is likewise implementation-defined.

The standard library has a type called iostate, which might be implemented as an enumeration. (Other implementations are also possible; see <ios> in Chapter 13 for details.) The enumeration has four enumerators, which can be used as bits in a bitmask and combined using bitwise operators:

enum iostate { goodbit=0, failbit=1, eofbit=2, badbit=4 };

The iostate enumeration can clearly fit in a char because the range of values is 0 to 7, but the compiler is free to use int, short, char, or the unsigned flavors of these types as the underlying type.

Because enumerations can be promoted to integers, any arithmetic operation can be performed on enumerated values, but the result is always an integer. If you want to permit certain operations that produce enumeration results, you must overload the operators for your enumerated type. For example, you might want to overload the bitwise operators, but not the arithmetic operators, for the iostate type in the preceding example. The sign type does not need any additional operators; the comparison operators work just fine by implicitly converting sign values to integers. Other enumerations might call for overloading ++ and -- operators (similar to the succ and pred functions in Pascal). Example 2-10 shows how operators can be overloaded for enumerations.

Example 2-10. Overloading operators for enumerations
// Explicitly cast to int, to avoid infinite recursion.
inline iostate operator|(iostate a, iostate b) {
  return iostate(int(a) | int(b));
}
inline iostate& operator|=(iostate& a, iostate b) {
  a = a | b;
  return a;
}
// Repeat for &, ^, and ~.

int main(  )
{
  iostate err = goodbit;
  if (error(  ))
    err |= badbit;
}

2.5.3 POD Types

POD is short for Plain Old Data. The fundamental types and enumerated types are POD types, as are pointers and arrays of POD types. You can declare a POD class, which is a class or struct that uses only POD types for its nonstatic data members. A POD union is a union of POD types.

POD types are special in several ways:

  • A POD object can be copied byte for byte and retain its value. In particular, a POD object can be safely copied to an array of char or unsigned char, and when copied back, it retains its original value. A POD object can also be copied by calling memcpy; the copy has the same value as the original.

  • A local POD object without an initializer is uninitialized, that is, its value is undefined. Similarly, a POD type in a new expression without an initializer is uninitialized. (A non-POD class is initialized in these situations by calling its default constructor.) When initialized with an empty initializer, a POD object is initialized to 0, false, or a null pointer.

  • A goto statement can safely branch across declarations of uninitialized POD objects. (See Chapter 4 for more information.)

  • A POD object can be initialized using an aggregate initializer. (See Section 2.6.3 later in this chapter for details.)

See Chapter 6 for more information about POD types, especially POD classes.

2.5.4 typedef Declarations

A typedef declares a synonym for an existing type. Syntactically, a typedef is like declaring a variable, with the type name taking the place of the variable name, and with a typedef keyword out in front. More precisely, typedef is a specifier in a declaration, and it must be combined with type specifiers and optional const and volatile qualifiers (called cv-qualifiers), but no storage class specifiers. A list of declarators follow the specifiers. (See the next section, Section 2.6, for more information about cv qualifiers, storage class specifiers, and declarators.)

The declarator of a typedef declaration is similar to that for an object declaration, except you cannot have an initializer. The following are some examples of typedef declarations:

typedef unsigned int uint;
typedef long int *long_ptr;
typedef matrix[3][3] matrix;
typedef void (*thunk)(  );
typedef signed char schar;

By convention, the typedef keyword appears before the type specifiers. Syntactically, typedef behaves as a storage class specifier (see Section 2.6.1 later in this chapter for more information about storage class specifiers) and can be mixed in any order with other type specifiers. For example, the following typedefs are identical and valid:

typedef unsigned long ulong;     // Conventional
long typedef int unsigned ulong; // Valid, but strange

A typedef is especially helpful with complicated declarations, such as function pointer declarations and template instantiations. They help the author who must concoct the declarations, and they help the reader who must later tease apart the morass of parentheses, asterisks, and angle brackets. The standard library uses them frequently. (See also Example 2-11.)

typedef std::basic_string<char, std::char_traits<char> > string;

A typedef does not create a new type the way class and enum do. It simply declares a new name for an existing type. Therefore, function declarations for which the parameters differ only as typedefs are not actually different declarations. The two function declarations in the following example are really two declarations of the same function:

typedef unsigned int uint;
uint func(uint);             // Two declarations of the
unsigned func(unsigned);     // same function

Similarly, because you cannot overload an operator on fundamental types, you cannot overload an operator on typedef synonyms for fundamental types. For example, both the following attempts to overload + result in an error:

int operator+(int, int);              // Error
typedef int integer;
integer operator+(integer, integer);  // Error

C programmers are accustomed to declaring typedefs for struct, union, and enum declarations, but such typedefs are not necessary in C++. In C, the struct, union, and enum names are separate from the type names, but in C++, the declaration of a struct, union, class, or enum adds the type to the type names. Nonetheless, such a typedef is harmless. The following example shows typedef being used to create the synonym point for the struct point:

struct point { int x, y; }
typedef struct point point; // Not needed in C++, but harmless
point pt;

2.5.5 Elaborated Type Specifier

An elaborated type specifier begins with an introductory keyword: class, enum, struct, typename, or union. The keyword is followed by a (possibly) qualified name of a suitable type. That is, enum is followed by the name of an enumeration, class is followed by a class name, struct by a struct name, and union by a union name. The typename keyword is used only in templates (see Chapter 7) and is followed by a name that must be a type name in a template instantiation.

A typename-elaborated type specifier is often needed in template definitions. The other elaborated type specifiers tend to be used in headers that must be compatible with C or in type names that have been hidden by other names. For example:

enum color { black, red };
color x;                     // No need for elaborated name
enum color color(  );        // Function hides color
enum color c = color(  );    // Elaborated name is needed here
    Team LiB   Previous Section   Next Section