2.5 Type DeclarationsOne 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 TypesThis 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).
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.
2.5.2 Enumerated TypesAn 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); 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). 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 }; 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 TypesPOD 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:
See Chapter 6 for more information about POD types, especially POD classes. 2.5.4 typedef DeclarationsA 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 SpecifierAn 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 |