C++ today is very different from what it was in 1983, when it was first named "C++". Many features have been added to the language since then; older features have been modified, and a few features have been deprecated or removed entirely from the language. Some of the extensions have radically changed programming styles and concepts. For example, downcasting a base to a derived object was considered a bad and unsafe programming practice before the standardization of Runtime Type Information. Today, downcasts are safe, and sometimes even unavoidable. The list of extensions includes const member functions, exception handling, templates, new cast operators, namespaces, the Standard Template Library, bool type, and many more. These have made C++ the powerful and robust multipurpose programming language that it is today.
The evolution of C++ has been a continuous and progressive process, rather than a series of brusque revolutions. Programmers who learned C++ only three or five years ago and haven't caught up with the extensions often discover that the language slips through their fingers: Existing pieces of code do not compile anymore, others compile with a plethora of compiler warnings, and the source code listings in object-oriented magazines seem substantially different from the way they looked not so long ago. "Namespaces? never heard of these before," and "What was wrong with C-style cast? Why shouldn't I use it anymore?" are some of the frequently asked questions in various C++ forums and conferences.
But even experienced C++ programmers who have kept up with changes by subscribing to newsgroups, reading magazines and books, or exchanging emails with the company's guru might still find that the C++ nomenclature in professional literature is sometimes unclear. The ANSI/ISO Standard is written in a succinct and technical jargon that is jocularly called standardese -- which is anything but plain English. For instance, the One Definition Rule (article 3.2 in the Standard), which defines under what conditions separate definitions of the same entity are valid, is explained in textbooks in a simpler -- although sometimes less accurate -- manner, when compared to the Standard text. The use of standardese ensures the accuracy that is needed for writing compilers and checking the validity of programs. For this purpose, the Standard defines numerous specific terms that are used extensively throughout the volume; for instance, it distinguishes between a template id and a template name, whereas an average programmer simply refers to both as templates. Familiarity with these specific terms is the key to reading and interpreting the Standard correctly.
The purposes of this chapter are threefold. First, it presents some of the key terms that are used extensively throughout the Standard and throughout this book, for example, undefined behavior and deprecated features. (Note that topic-specific terms such as argument-dependent lookup and trivial constructor are presented in their relevant chapters rather than here.) Then, the new features that have been added to C++ -- such as bool type, new typecast operators, and mutable data members -- are discussed. Because these topics are not explained elsewhere in this book, they are presented here in detail, along with code samples. After that comes a list of other newly added features that are covered extensively in other chapters of the book.
These topics are presented here only briefly. The intent is to provide you with an executive summary -- a panorama of the latest addenda to the ANSI/ISO C++ Standard -- that you can use as a checklist of topics. When reading the brief topics overview, you might come across an unfamiliar topic; in these instances, you are always referred to the chapter that discusses the topic in further detail. Finally, there is an overview the deprecated features and a list of suggested replacements for them.
This part explains some of the key terms in the Standard that are used throughout the book. These terms appear in italics when they are presented for the first time. Note that these definitions are not exact quotations from the Standard; rather, they are interpretive definitions.
The words arguments and parameters are often used interchangeably in the literature, although the Standard makes a clear distinction between the two. The distinction is chiefly important when discussing functions and templates.
An argument is one of the following: an expression in the comma-separated list that is bound by the parentheses in a function call; a sequence of one or more preprocessor tokens in the comma-separated list that is bound by the parentheses in a function-like macro invocation; the operand of a throw-statement or an expression, type, or template-name in the comma-separated list that is bound by the angle brackets in a template instantiation. An argument is also called an actual parameter.
A parameter is one of the following: an object or reference that is declared in a function declaration or definition (or in the catch clause of an exception handler); an identifier from the comma-separated list that is bound by the parentheses immediately following the macro name in a definition of a function-like macro; or a template-parameter. A parameter is also called a formal parameter.
The following example demonstrates the difference between a parameter and an argument:
void func(int n, char * pc); //n and pc are parameters template <class T> class A {}; //T is a a parameter int main() { char c; char *p = &c; func(5, p); //5 and p are arguments A<long> a; //'long' is an argument A<char> another_a; //'char' is an argument return 0; }
A translation unit contains a sequence of one or more declarations. The Standard uses the term translation unit rather than source file because a single translation unit can be assembled from more than a single source file: A source file and the header files that are #included in it are a single translation unit.
A program consists of one or more translation units that are linked together.
A well-formed program is one that is constructed according to the Standard's syntax and semantic rules and that obeys the One Definition Rule (explained in the following section). An ill-formed program is one that does not meet these requirements.
An object is a contiguous region of storage. An lvalue is an expression that refers to such an object. The original definition of lvalue referred to an object that can appear on the left-hand side of an assignment. However, const objects are lvalues that cannot be used in the left-hand side of an assignment. Similarly, an expression that can appear in the right-hand side of an expression (but not in the left-hand side of an expression) is an rvalue. For example
#include <string> using namespace std; int& f(); void func() { int n; char buf[3]; n = 5; // n is an lvalue; 5 is an rvalue buf[0] = 'a'; // buf[0] is an lvalue, 'a' is an rvalue string s1 = "a", s2 = "b", s3 = "c"; // "a", "b", "c" are rvalues s1 = // lvalue s2 +s3; //s2 and s3 are lvalues that are implicitly converted to rvalues s1 = //lvalue string("z"); //temporaries are rvalues int * p = new int; //p is an lvalue; 'new int' is an rvalue f() = 0; //a function call that returns a reference is an lvalue s1.size(); //otherwise, a function call is an rvalue expression }
An lvalue can appear in a context that requires an rvalue; in this case, the lvalue is implicitly converted to an rvalue. An rvalue cannot be converted to an lvalue. Therefore, it is possible to use every lvalue expression in the example as an rvalue, but not vice versa.
The Standard lists several types of program behaviors, which are detailed in the following sections.
Implementation-defined behavior (for a well-formed program and correct data) is one that depends on the particular implementation; it is a behavior that each implementation must document. For example, an implementation documents the size of fundamental types, whether a char can hold negative values, and whether the stack is unwound in the case of an uncaught exception. Implementation-defined behavior is also called implementation-dependent behavior.
Unspecified behavior (for a well-formed program and correct data) is one that depends on the particular implementation. The implementation is not required to document which behavior occurs (but it is allowed to do so). For example, whether operator new calls to the Standard C library function malloc() is unspecified. Following is another example: The storage type for the temporary copy of an exception object is allocated in an unspecified way (however, it cannot be allocated on the free store).
Implementation-defined behavior and unspecified behavior are similar. Both refer to consistent behavior that is implementation-specific. However, unspecified behavior usually refers to the underlying mechanism of the implementation, which users generally do not access directly. Implementation-dependent behavior refers to language constructs that can be used directly by users.
Undefined behavior is one for which the Standard imposes no requirements. This definition might sound like an understatement because undefined behavior indicates a state that generally results from an erroneous program or erroneous data. Undefined behavior can be manifested as a runtime crash or as an unstable and unreliable program state -- or it might even pass unnoticed. Writing to a buffer past its boundary, accessing an out-of-range array subscript, dereferencing a dangling pointer, and other similar operations result in undefined behavior.
Unspecified behavior and implementation-defined behavior are consistent -- albeit nonportable -- behaviors that are left intentionally unspecified by the C++ Standard, usually to allow efficient and simple compiler implementation on various platforms. Conversely, undefined behavior is always undesirable and should never occur.
A class, an enumeration, an inline function with external linkage, a class template, a nonstatic function template, a member function template, a static data member of a class template, or a template specialization for which some template parameters are not specified can be defined more than once in a program -- provided that each definition appears in a different translation unit, and provided that the definitions meet the requirements that are detailed in the following sections.
Each definition must contain the same sequence of tokens. For example
//file fisrt.cpp inline int C::getVal () { return 5; } //file sec.cpp typedef int I; inline I C::getVal () { return 5; } // violation of ODR, // I and int are not identical tokens
On the other hand, white spaces and comments are immaterial:
//file fisrt.cpp inline int C::getVal () { return 5; } //file sec.cpp inline int C::getVal () { /*complies with the ODR*/ return 5; }
Each token in the identical sequences of the separate definitions has the same semantic contents. For example
//file first.cpp typedef int I; inline I C::getVal () { return 5; } //file second.cpp typedef unsigned int I; inline I C::getVal () { return 5; } //error; different semantic content for I
A name that refers to an object, reference, type, function, template, namespace, or value that is declared in another scope is said to have linkage. The linkage can be either external or internal. Otherwise, the name has no linkage.
A name that can be referred to from other translation units or from other scopes of the translation unit in which it was defined has external linkage. Following are some examples:
void g(int n) {} //g has external linkage int glob; //glob has external linkage extern const int E_MAX=1024; //E_MAX has external linkage namespace N { int num; //N::num has external linkage void func();//N::func has external linkage } class C {}; //the name C has external linkage
A name that can be referred to by names from other scopes in the translation unit in which it was declared, but not from other translation units, has internal linkage. Following are some examples:
static void func() {} //func has internal linkage union //members of a non-local anonymous union have internal linkage { int n; void *p; }; const int MAX=1024; //non-extern const variables have internal linkage typedef int I; //typedefs have internal linkage
A name that can only be referred to from the scope in which it is declared has no linkage. For example
void f() { int a; //a has no linkage class B {/**/}; //a local class has no linkage }
A side effect is a change in the state of the execution environment. Modifying an object, accessing a volatile object, invoking a library I/O function, and calling a function that performs any of these operations are all side effects.
This part details the new features -- and extensions to existing features -- that have been adopted by the C++ Standard in recent years.
C++ still supports C-style cast, as in
int i = (int) 7.333;
Nonetheless, C-style cast notation is problematic for several reasons. First, the operator () is already used excessively in the language: in a function call, in precedence reordering of expressions, in operator overloading, and in other syntactic constructs. Second, C-style cast carries out different operations in different contexts -- so different that you can hardly tell which is which. It can perform an innocuous standard cast, such as converting an enum value to an int; but it can also cast two nonrelated types to one another. In addition, a C-style cast can be used to remove the const or volatile qualifiers of an object (and in earlier stages of C++, the language was capable of performing dynamic casts as well).
Because of these multiple meanings, the C-style cast is opaque. Sometimes it is very difficult for a reader to clearly understand the intent of the code author who uses a C-style cast operation. Consider the following example:
#include <iostream> using namespace std; void display(const unsigned char *pstr) { cout<<pstr<<endl; } void func() { const char * p = "a message"; display( (unsigned char*) p); //signed to unsigned cast is required // but const is also removed. was that // intentional or a programmer's oversight? }
The new cast operators make the programmer's intention clearer and self-documenting. In addition, they enable the compiler to detect mistakes that cannot be detected with C-style cast. The new cast operators are intended to replace C-style cast; C++ programmers are encouraged to use them instead of C-style cast notation. These operators are detailed in the following sections.
static_cast <Type> (Expr) performs well-behaved and reasonably well-behaved casts. One of its uses is to indicate explicitly a type conversion that is otherwise performed implicitly by the compiler.
For example
class Base{}; class Derived : public Base {}; void func( Derived * pd) { Base * pb = static_cast<Base *> (pd); //explicit }
A pointer to a derived object can be automatically converted to a pointer to a public base. The use of an explicit cast makes the programmer's intent clearer.
static_cast can be used to document user-defined conversion. For example
class Integer { public: operator int (); }; void func(Integer& integer) { int num = static_cast<int> (integer); //explicit }
Narrowing conversions of numeric types can also be performed explicitly by a static_cast. For example
void func() { int num = 0; short short_num = static_cast<short> (num); }
This conversion is somewhat riskier than the previous conversions. A short might not represent all the values that an int can hold; an explicit cast is used here, instead of an implicit conversion, to indicate that a type conversion is performed. Casting an integral value to an enum is also a dangerous operation because there is no guarantee that the value of an int can be represented in an enum. Note that in this case, an explicit cast is necessary:
void func() { enum status {good, bad}; int num = 0; status s = static_cast<status> (num); }
You can use static_cast to navigate through class hierarchies. Unlike dynamic_cast, however, it relies solely on the information that is available at compile time -- so don't use it instead of dynamic_cast. Using static_cast for this purpose is safer than using C-style cast because it does not perform conversions between nonrelated classes. For example
class A{}; class B{}; A *pa; B * pb = static_cast<B *> (pa); //error, pointers are not related
const_cast <T> (Expr) removes only the const or volatile qualifiers of Expr and converts them to type T. T must be the same type of Expr, except for the missing const or volatile attributes. For example
#include <iostream> using namespace std; void print(char *p) //parameter should have been declared as const; alas, { cout<<p; } void f() { const char msg[] = "Hello World\n"; char * p = const_cast<char *> (msg); //remove constness print(p); }
const_cast can also convert an object to a const or volatile one:
void read(const volatile int * p); int *p = new int; read( const_cast<const volatile int *> (p) ); //explicit
Note that the removal of the const qualifier of an object does not guarantee that its value can be modified; it only guarantees that it can be used in a context that requires a non-const object. So that you understand these limitations, the following sections examines const semantics in further detail.
(f)const Semantics
There are actually two types of const: true const and contractual const. A true const object is an lvalue that was originally defined as const. For example
const int cn = 5; // true const const std::string msg("press any key to continue"); // true const
On the other hand, an object with contractual const quality is one that was defined without the const qualifier, but that is treated as though it were const. For example
void ReadValue(const int& num) { cout<<num; // num may not be modified in ReadValue() } int main() { int n =0; ReadValue(n); //contractual const, n is treated as if it were const }
When a true const variable is explicitly cast to a non-const variable, the result of an attempt to change its value is undefined. This is because an implementation can store true const data in the read-only memory, and an attempt to write to it usually triggers a hardware exception. (Using an explicit cast to remove constness does not change the physical memory properties of a variable.) For example
const int cnum = 0; //true const, may be stored in the machine's ROM const int * pci = &cnum; int *pi = const_cast<int*> (pci); // brute force attempt to unconst a variable cout<< *pi; //OK, value of cnum is not modified *pi = 2; //undefined, an attempt to modify cnum which is a true const variable
On the other hand, casting away the contractual constness of an object makes it possible to modify its value safely:
int num = 0; const int * pci = # // *pci is a contractual const int int *pi = const_cast<int*> (pci); // get rid of contractual const *pi = 2; // OK, modify num's value
To conclude, const_cast is used to remove the const or volatile qualities of an object. The resultant value can be used in a context that requires a non-const or volatile object. The cast operation is safe as long as the resultant value is not modified. It is possible to modify the value of the resultant object only if the original operand is not truly const.
reinterpret_cast <to> (from) is used in low-level, unsafe conversions. reinterpret_cast merely returns a low-level reinterpretation of the bit pattern of its operand. Note, however, that reinterpret_cast cannot alter the cv-qualification of its operand. The use of reinterpret_cast is dangerous and highly non-portable -- use it sparingly. Following are examples of reinterpret_cast uses.
reinterpret_cast can be used to convert two pointers of completely nonrelated types, as in
#include <cstdio> void mem_probe() { long n = 1000000L; long *pl = &n; unsigned char * pc = reinterpret_cast <unsigned char *> (pl); printf("%d %d %d %d", pc[0], pc[1], pc[2], pc[3]); //memory dump }
reinterpret_cast can cast integers to pointers, and vice versa. For example
void *pv = reinterpret_cast<void *> (0x00fffd); int ptr = reinterpret_cast<int> (pv);
reinterpret_cast can also be used for conversions between different types of function pointers. The result of using the resultant pointer to call a function with a nonmatching type is undefined.
Do not use reinterpret_cast instead of static_cast -- the results might be undefined. For example, using reinterpret_cast to navigate through the class hierarchy of a multiply-inherited object is likely to yield the wrong result. Consider the following:
class A { private: int n; }; class B { private: char c; }; class C: public A, public B {}; void func(B * pb) { C *pc1 = static_cast<C*> (pb); //correct offset adjustment C *pc2 = reinterpret_cast<C*> (pb); //no offset calculated } int main() { B b; func(&b); }
On my machine, pc1 is assigned the value 0x0064fdf0, whereas pc2 is assigned 0x0064fdf4. This demonstrates the difference between the two cast operators. Using the information that is available at compile time, static_cast converts a pointer to B to a pointer to C. It does so by causing pc1 to point at the start of C by subtracting the offset of the subobject B. On the other hand, reinterpret_cast simply assigns the binary value of pb to pc2, without any further adjustments; for this reason, it yields the wrong result.
In pre-standard C++, as was noted earlier, C-style cast was used to perform a dynamic cast as well. The cast was either static or dynamic, depending on the type of the operand. The Standardization committee, however, opposed this approach. An expensive runtime operation that looked exactly like a static cast (that is, penalty-free) can mislead the users. For this purpose, a new operator was introduced to the language: dynamic_cast (dynamic_cast is discussed in further detail in Chapter 7, "Runtime Type Identification"). The name and the syntax of dynamic_cast were chosen to look markedly different from C-style cast. All other new typecast operators follow this model. Following is an example of dynamic_cast:
Derived *p = dynamic_cast<derived *> (&base); //pointer form Derived & rd = dynamic_cast<derived &> (base); //reference form
The new typecasting operators are clearer and more explicit in their intended purpose. A name such as dynamic_cast, for example, warns its users about its incurred runtime overhead. Most importantly, though, the new cast operators are safer because they give the compiler a chance to detect the programmer's mistakes.
Users might find the proliferation of cast operators somewhat confusing. In particular, the choice between static_cast and reinterpret_cast might not seem immediately clear. How to choose? As a rule, static_cast is the first choice. If the compiler refuses to accept it, use reinterpret_cast instead.
The built-in bool data type was added to the Standard after consideration of several other proposals. None of these was found satisfactory. Following is an overview some of these proposals, which is in turn followed by a discussion of the characteristics of the bool type.
One suggestion was to use a typedef for a Boolean data type:
typedef int bool;
However, a typedef that relies on another built-in type of the language renders the Boolean type unusable with some language features. For example, using it in function overloading can result in ambiguities:
void f(bool); void f(int);//error, redefinition of void f(bool);
In addition, a typedef is not strongly-typed. Consequently, it is impossible to ensure that only Boolean values are assigned to it in a context that requires Boolean values.
An alternative solution was to use an enum type:
enum bool { false, true};
enums are strongly-typed. However, the committee wanted to ensure backward compatibility with old code that used int values as a Boolean data type. For example
#include <ctype.h> enum bool {false, true}; void f() { enum bool b; b = islower('a'); //compile time error, int assigned to an enum }
A third suggestion was to use a class such as the following:
class bool { private: int val; public: operator int(); };
Such a class guarantees type uniqueness, so it can be used to overload functions and to specialize templates. In addition, it is backward compatible with Boolean integers. There are, however, several drawbacks to the class approach. First, users are required to #include a dedicated header and to link their programs with the compiled code of the class. Worse yet, the conversion operator might interfere with user-defined conversion operators that are defined in other classes. Finally, a full-blown class that defines constructors and conversion operators is significantly less efficient than a fundamental type. For these reasons, the Standardization committee decided to add a new built-in type.
bool is an implementation-dependent integral type that can hold either a true or a false value. A standardized Boolean type has several advantages:
Portability -- All Standard compliant compilers support bool type. When code is ported to different platforms, it will work as expected.
void f(bool b); void f(int n);
With the introduction of the bool data type, built-in operators were modified accordingly to work with bool values. The logical operators &&, ||, and ! now take bool values as arguments and return bool results. Similarly, the relational operators <, >, <=, >=, ==, and != return bool results. In addition, iostream classes were adjusted to support the new type.
(f)Viewing bool Variables as Literals
By default, iostream objects display bool variables as 0 and 1. It is possible to override the default setting by inserting the formatting flag boolalpha to the stream object. Subsequently, the symbolic representations false and true are displayed instead of 0 and 1. For example
#include <iostream> using namespace std; int main() { bool b = true; cout<<b; // default setting; display 1 cout<<boolalpha; //henceforth, display 'true' and 'false' instead of 1 and 0 cout<<b; // output: true cout<<!b; // output: false return 0; }
Exception handling is used to report and handle runtime errors. Supplementary features, namely exception specifications and function try blocks, were added to the Standard in recent years. The following sections provide a brief overview of these features. (Exception handling and the supplementary features are discussed in more detail in Chapter 6, "Exception Handling.")
A function can indicate the potential exceptions it can throw by specifying a list of these exceptions. Exception specifications are particularly useful when users of such a function can only view its prototype but cannot access its source file. Following is an example of specifying an exception:
class Zerodivide{/*..*/}; int divide (int, int) throw(Zerodivide); //function may throw an exception //of type Zerodivide, but no other
A function try block is a function whose body consists of a try block and its associated handlers. A function try block enables you to catch exceptions that might be thrown by a base class constructor or by a constructor of a member object. The original specification of exception handling did not enable users to handle exceptions thrown from a constructor or a member initialization list locally; a function try block fixes this loophole. Following is an example of a function try block:
class Err{}; A::A(const string& s) throw (Err); //allowed to throw only //an exception of type Err try : str(s) //str's constructor might throw a bad_alloc //exception, which violates C's exception specification { // constructor function body } catch (...) //we get here when an exception is thrown //during the construction of str or C { throw Err(); //replace bad_alloc exception with an Err exception }
The Standard now defines three different versions of operator new: plain new, nothrow new, and placement new. Each of these operators has an array version as well. The Standard also defines six matching types of operator delete that correspond the specific versions of new. Memory management and the recently added versions of new and delete are discussed in further detail in Chapter 11, "Memory Management."
In earlier stages of C++, operator new returned a NULL pointer when it failed to allocate the requested amount of memory. The C++ standardization committee changed the specification of operator new so that it throws an exception of type std::bad_alloc, rather than returning a NULL pointer, when it fails. A program that uses operator new directly or indirectly has to handle a potential std::bad_alloc exception. For example
void f(int size) //standard-conforming usage of operator new { char *p = new char [size]; //...use p safely delete [] p; return; } #include <stdexcept> #include <iostream> using namespace std; const int BUF_SIZE = 1048576L; int main() { try { f(BUF_SIZE); } catch(bad_alloc& ex) //handle exception thrown from f() { cout<<ex.what()<<endl; //...other diagnostics and remedies } return -1; }
The Standard also defines an exception-free version of operator new, which returns a NULL pointer in case of a failure rather than throwing an exception. This version of new takes an additional argument named nothrow. For example
#include <new> #include <string> using namespace std; void f(int size) // demonstrating nothrow new { char *p = new (nothrow) char [size]; //array nothrow new if (p == 0) { //...use p delete [] p; } string *pstr = new (nothrow) string; //plain nothrow new if (pstr == 0) { //...use pstr delete [] pstr; } return; }
An additional version of operator new enables the user to construct an object at a predetermined memory position. This version is called placement new. Following is an example of using placement new:
#include <new> #include <iostream> using namespace std; void placement() { int *pi = new int; //plain new int *p = new (pi) int (5); //placement new //...use p delete pi; }
Fundamental types can be initialized by a special constructor. In addition, the Standard also defines a pseudo destructor for each of these types (see Chapter 4, "Special Member Functions: Default Constructor, Copy Constructor, Destructor, and Assignment Operator").
Variables of fundamental types can be initialized by invoking their constructor explicitly. For example
void f() { int n = int(); // zero initialized char c = char(); // also zero initialized double d = double(0.5); //other initializers are allowed }
This language extension enables uniform treatment in templates for fundamental types and user-defined types.
A constructor that takes a single argument is, by default, an implicit conversion operator that converts its argument to an object of its class. In order to avoid such implicit conversions, a constructor that takes one argument can be declared explicit. For example
class C { public: explicit C(int size); // disallow implicit conversion };
A pseudo destructor is a syntactic construct whose sole purpose is to satisfy the need for generic algorithms and containers. It is a no-op code, and has no real effect on its object. For example
typedef int N; void f() { N i = 0; i.N::~N(); // pseudo destructor invocation i = 1; // i was not affected by the invocation of the pseudo destructor }
The scoping rules for a variable that is defined in a for statement were changed. Additionally, it is now possible to define and initialize variables inside an if condition.
C++ allows declaration of variables wherever they are needed, enabling immediate initializations. A good example is a loop counter, which can be declared inside a for statement. For example
void f() { for (int i = 0; i < 10; i++) // i declared and initialized // inside a for-statement { cout << i <<endl; //output 0 to 9 } int n = i; //compilation error, i not in scope }
In earlier stages of C++, a local variable declared in this way remained accessible in its enclosing block. This was a source for bugs and name hiding. Consequently, the standard has been revised to fix this loophole, so to speak; local variables that are created this way are inaccessible outside their for statement. In the preceding example, the variable i goes out of scope when the loop is exited.
You can define and initialize a variable inside the condition of an if statement. For example
class Base {/*..*/}; class Derived: public Base {/*..*/}; void func (Base& b) { if ( Derived *pd = dynamic_cast < Derived* > (&b) ) //declaration // inside an if-condition { //dynamic_cast was successful; use pd here return; }//pd goes out of scope at this point //otherwise dynamic_cast failed; variable pd is not in scope }
The advantage of declaring the pointer pd locally is obvious: It is always initialized with an appropriate value, and it isn't visible to other parts of the program that are not to use it (see Chapter 12, "Optimizing Your Code").
Namespaces were the latest feature to be added to the language. Namespaces are used to prevent name conflicts and to facilitate configuration management and version control in large-scale projects. Most of the components of the Standard Library are grouped under namespace std. There are three methods for injecting namespace members into a scope: a using directive, a using declaration, or a fully qualified name. Argument-dependent lookup, or Koenig lookup, simplifies the use of namespaces by automating the name lookup process. Namespaces are discussed in more detail in Chapter 8, "Namespaces."
A template is a mold from which related functions or classes are instantiated. Templates have come a long way since they were first introduced to the language in 1991. Back then, they were merely clever macros. However, the adoption of STL required considerable extensions to this feature. An overview of these extension is provided in the following sections. Templates are discussed in detail in Chapter 9, "Templates."
A template can now take a template as an argument. For example
int send(const std::vector<char*>& ); int main() { std::vector <std::vector<char*> > msg_que(10); //...fill msg_que for (int i =0; i < 10; i++) //transmit messages send(msg_que[i]); return 0; }
Templates can have default type arguments. For example
template <class T, class S = size_t > class C //using a default type {/**/};
Templates can be nested; a template can be declared within another class or a class template. Such a template is called a member template. Following is an example:
template<class T> class C { public: template<class T2> int func(const T2&); //declaration of a member template //... }; template<class T> template<class T2> int C<T>::func(const T2& s) // definition { //... }
To support member templates and template inheritance, the keyword typename was added to the language. By default, the compiler assumes that a qualified name refers to a non-type. The typename keyword instructs the compiler to supersede this default interpretation and resolve the ambiguity in favor of a typename instead.
It is possible to compile a template definition only once, and to subsequently use only the template's declaration in other translation units. To compile a template separately and then use its declaration, the template has to be exported. This is done by preceding the template's definition with the keyword export.
According to Bjarne Stroustrup, the most important change in C++ since 1991 is not a language change; it is the addition of the standard library. The Standard Template Library, or STL, comprises a substantial part of the Standard Library. STL is collection of generic containers -- such as vector, list, and stack -- and a rich collection of generic algorithms for sorting, finding, merging, and transforming these containers. Chapter 10, "STL and Generic Programming," is dedicated to STL.
The current C++ Standard is an international Standard approved by ISO. To qualify as such, Standard C++ has been fully internationalized. Internationalization consists of several modifications and extensions. These include the addition of the keyword wchar_t (wchar_t was already defined in ISO C as a typedef but it wasn't a reserved keyword). In addition, the standard stream and string classes have been templatized to support both narrow and wide characters. Finally, the <locale> library defines template classes and declares functions that encapsulate and manipulate locale-related information such as monetary, numeric, and time conventions. The locale feature sets are encapsulated in classes (or facets) that the users can extend.
C++ provides four standard I/O streams that are automatically instantiated before a program's outset. They are defined in the header <iostream>:
cin // standard input stream of char cout // standard output stream of char cerr // standard unbuffered output stream for error messages clog // standard output stream for error messages
Each of these four streams now has a corresponding wide-character version:
wcin wcout wcerr wclog
Two additional extensions were recently added to the Standard: the initialization of const static data members inside the class body and the mutable storage specifier. Both of these extensions are discussed in the following sections.
const static data members of an integral type can now be initialized inside their class. In this case, the initialization is also a definition, so no further definitions are required outside the class body. For example
#include <string> class Buff { private: static const int MAX = 512; // initialization +definition static const char flag = 'a'; // initialization +definition static const std::string msg; //non-integral type; must be defined outside //the class body //.. }; const std::string Buff::msg = "hello";
A const member function cannot modify the state of its object. However, auxiliary data members (flags, reference counters) sometimes have to be modified by a const member function. Such data members can be declared mutable. A mutable member is never const, even if its object is const; therefore, it can be modified by a const member function. The following example demonstrates the use of this feature:
class Buffer { private: void * data; //raw data buffer to be transmitted on some network size_t size; mutable int crc;//used to verify that no errors occurred during transmission public: int GetCrc() const; void Transmit() const; //computation of crc is done here }; void f() { Buffer buffer; //...fill buffer with data buffer.Transmit(); //crc can be modified here; non-mutable members may not }
There is no point in calculating the crc value every time a few more bytes are appended to buffer. Instead, it is computed by the member function Transmit() right before buffer is sent. However, Transmit() is not supposed to modify its object's state so that it is declared as a const member function. In order to allow assignment of the correct crc value, the data member crc is declared mutable; hence, it can be modified by a const member function.
A deprecated feature is one that the current edition of the Standard regards as normative, but that is not guaranteed to be part of the Standard in future revisions. Again, this is somewhat understating the intent of this definition. One of the consequences of the evolution and standardization of a programming language is the gradual removal of undesirable, dangerous, or redundant features and constructs. By deprecating a feature, the standardization committee expresses the desire to have the feature removed from the language. Removing it from the language altogether is impractical because existing code will not compile anymore. The deprecation gives the user sufficient time to replace a deprecated feature with a Standard-endorsed feature. The Standard lists the features that are discussed in the following sections as deprecated.
Applying postfix ++ to a variable of type bool is still allowed for backward compatibility with old code that uses plain int or some typedef, as in
bool done = false; while(!done) { if(condition) //...do something else done++; //deprecated }
Incrementing a bool always yields true, regardless of the original value. However, this practice is deprecated; therefore, it might not be supported in the future. Remember that applying -- (decrement operator) to a bool variable is illegal.
The use of the keyword static to declare a function or an object local to a translation unit is deprecated. Instead, use an unnamed namespace for that purpose (more on this in Chapter 8).
The access of a member of a base class can be changed in a derived class by an access declaration. For example
class A { public: int k; int l; }; class D : public A { private: A::l; // access declaration changes access of A::l to private; deprecated };
The use of access declarations is deprecated. Use a using declaration instead:
class D : public A // using-declaration version { private: using A::l; // using declaration changes the access of A::l to private }; void func() { D d; d.l = 0; //error; cannot access private member }
A string literal can be implicitly converted from type pointer to const char to type pointer to char. Similarly, a wide string literal can be implicitly converted from type pointer to const wchar_t to pointer to wchar_t. For example
char *s = "abc"; //string literal implicitly converted to non-const; deprecated
The type of the literal "abc" is const char[], but s is a pointer to non-const char. Such implicit conversions from const to non-const qualifications for string literals are deprecated because they might lead to the following erroneous code:
strcpy(s, "cde"); //undefined behavior
The preferred form is
const char *s = "abc"; //OK
For compatibility with the Standard C library, C++ still supports the naming convention of C headers in the form <xxx.h> -- but this naming convention is now deprecated (this is discussed in more detail in Chapter 8). For example
#include <stdlib.h> //deprecated
Use the newer naming convention, <cxxx>, instead:
#include <cstdlib> //OK, 'c' prefixed and ".h" omitted
The reason for this is that ".h" headers inject all their names into the global namespace, whereas the newer convention headers, <cname>, keep their names in namespace std.
The default type for missing declarations such as the following is int:
static k =0; //'int' type deduced; deprecated const c =0; //'int' type deduced; deprecated
This convention is now considered deprecated. Instead, use explicit type names:
static int k =5; const int c = 0;
The Standard deprecates some of the members of old iostream classes (article 4.6). In addition, it deprecates three types in the header <strstream> that associate stream buffers with character array objects (article 4.7).
From a user's point of view, deprecated features are not to be used in new code because future versions of the language will flag it as an error. In the interim period, compilers can issue a warning about its use.
As you have seen, standard C++ is quite different from what it used to be four, five, or ten years ago. Many of the changes and modifications have been initiated by software vendors (the bool data type, for instance). Others have been initiated by the Standardization committee (for example, the new cast operators and STL). During the hectic standardization process, complaints about "randomly accepted changes" on the one hand, and "missing features" on the other hand, were often heard. However, the Standardization committee has been very thoughtful and prudent in selecting these extensions and modifications. Fortunately, the approval of the Standard by ISO in 1998 ensures its stability for at least five years. This freeze period enables both compiler vendors and language users to digest the current Standard and use it effectively. In doing so, one usually finds that C++ is better today than it ever was.
© Copyright 1999, Macmillan Computer Publishing. All rights reserved.