C++ Primer 第五版 学习笔记 Part III
文章目录
Copy Control
A class controls what happens when objects of class type are copied, moved, assigned, and destroyed by defining five special member functions: copy constructor, move constructor, copy-assignment operator, move-assignment operator, destructor. The copy and move constructors define what happens when an object is initialized from another object of the same type. The copy and move assignment operators define what happens when we assign an object of a class type to another object of that same class type. The destructor defines what happens when an object ceases to exist. If a class does not define all of the copy-control members, the compiler automatically defines the missing operations.
The Copy Constructor
A constructor is the copy constructor if its first parameter is a reference to the class type and any additional parameters have default values. The first parameter must be a reference type, and it is almost always a reference to const, although we can define a reference to nonconst. Unlike the synthesized default constructor, a copy constructor is synthesized even if we define other constructors.
|
|
The Synthesized Copy Constructor
The type of each member determines how that member is copied: Members of class type are copied by the copy constructor for that class; members of built-in type are copied directly. Although we cannot directly copy an array, the synthesized copy constructor copies members of array type by copying each element. Elements of class type are copied by using the element’s copy constructor. For example, the synthesized copy constructor:
|
|
Copy Initialization
We are now in a position to fully understand the differences between direct initialization and copy initialization:
|
|
When using direct initialization, we are asking the compiler to use ordinary function matching to select the constructor that best matches the arguments we provide. When we use copy initialization, we are asking the compiler to copy the right-hand operand into the object being created, converting that operand if necessary.
Copy initialization ordinary uses the copy constructor. However, if a class has a move constructor, then copy initialization sometimes uses the move constructor instead of the copy constructor. Copy initialization happens not only when we define variables using an =, but also when we
- pass an object as an argument to a parameter of nonreference type
- return an object from a function that has a nonreference return type
- brace initialization the elements in an array or the members of an aggregate class
Some class types also use copy initialization for the objects they allocate. For example, the library containers copy initialize their elements when we initialize the container, or when we call an insert or push member. By contrast, elements created by an emplace member are direct initialized.
During a function call, parameters that have a nonreference type are copy initialized. This explains why the copy constructor’s own parameters must be a reference. If that parameter were not a reference, then the call would never succeed.
Whether we use copy or direct initialization matters if we use an initializer that requires conversion by an explicit constructors:
|
|
If we want to use an explicit constructor, we must do so explicitly.
During copy initialization, the compiler is permitted (but not obligated) to skip the copy/move constructor and create the object directly. That is, the compiler is permitted to rewrite
|
|
into
|
|
However, even if the compiler omits the call to the copy/move constructor, the copy/move constructor must exist and must be accessible at that point in the program.
The Copy-Assignment Operator
Overloaded operators are functions that have the name operator followed by the symbol for the operator being defined. Hence, the assignment operator is a function named operator=. The parameters in an overloaded operator represent the operands of the operator. When an operator is a member function, the left-hand operand is bound to implicit this parameter. The copy-assignment operator takes an argument of the same type as the class:
|
|
To be consistent with assignment for built-in types, assignment operators usually return a reference to left-hand operand. As an example, the floowing is equivalent to the synthesized Sales_data copy-assignment operator:
|
|
The Destructor
Constructors initialize the nonstatic data members of an object and may do other work; destructors do whatever work is needed to free the resources used by an object and destroy the nonstatic data members of the object. The destructor is a member function with the name of the class prefixed by a tilde (~). It has no return value and takes no parameters:
|
|
In a constructor, members are initialized before the function body is executed, and members are initialized in the same order as they appear in the class. In a destructor, the function body is executed first and then the members are destroyed. Members are destroyed in reverse order from the order in which they were initialized.
The built-in types do not have destructors, so nothing is done to destroy members of built-in type.
Unlike ordinary pointers, the smart pointers are class types and have destructors. As a result, members that are smart pointers are automatically destroyed during the destruction phase.
The destructor is used automatically whenever an object of its type is destroyed:
- variables are destroyed when they go out of scope
- members of an object are destroyed when the object of which they are a part is destroyed
- elements in a container or an array are destroyed when the container is destroyed
- dynamically allocated objects are destroyed when the delete operator is applied to a pointer to the object
- temporary objects are destroyed at the end of the full expression in which the temporary was created
Note: the destructor is not run when a reference or a pointer to an object goes out of scope.
The members are automatically destroyed after the destructor body is run. It is important to realize that the destructor body does not directly destroy the members. Members are destroyed as part of the implicit destruction phase that follows the destructor body.
The Rule of Copy Control Operations
One rule of thumb to use when you decide whether a class needs to define its own versions of the copy-control members is to decide first whether the class needs a destructor. If the class needs a destructor, it almost surely needs a copy constructor and copy-assignment operator as well. The following example just explain this:
|
|
To avoid using the synthesized copy constructor and copy-assignment operator for this class, we should define our own version:
|
|
A second rule of thumb: If a class needs a copy constructor, it almost surely needs a copy-assignment operator. And vice versa, if the class needs an assignment operator, it almost surely needs a copy constructor as well. Nevertheless, needing either the copy constructor or the copy-assignment operator does not indicate the need for a destructor.
Using = default
Under the new standard, we can explicitly ask the compiler to generate the synthesized versions of the copy-control members by defining them as = default.
|
|
When we specify = default on the declaration in the class body, the synthesized function is implicitly inline (just as any other member function defined in the body of the class). If we don’t want it inline, we can specify = default on the definition.
Preventing Copies
Under the new standard C++ 11, we can prevent copies by defining the copy constructor and copy-assignment operator as deleted functions. A deleted function is one that is declared but may not be used in any other way.
|
|
Unlike = default, = delete must appear on the first declaration of a deleted function. Also, we can specify = delete on any function. But the destructor should not be a deleted member, or that member cannot be destroyed.
For some classes, the compiler defines these synthesized members as deleted functions:
- synthesized destructor is defined as deleted if class has a member whose own destructor is deleted or is inaccessible (private).
- synthesized copy constructor is defined as deleted if the class has a member whose own copy constructor is deleted or inaccessible. It is also deleted if the class has a member with a deleted or inaccessible destructor.
- synthesized copy-assignment operator is defined as deleted if a member has a deleted or inaccessible copy-assignment operator, or if the class has a const or reference member.
- synthesized default constructor is defined as deleted if the class has a member with a deleted or inaccessible destructor; or has a reference member that does not have an in-class initializer; or has a const member whose type does not explicitly define a default constructor and that member does not have an in-class initializer.
In essence, these rules mean that if a class has a data member that cannot be default constructed, copied, assigned, or destroyed, then the corresponding member will be a deleted function.
It may be surprising that a member that has a deleted or inaccessible destructor causes the synthesized default and copy constructors to be defined as deleted. The reason is that without it, we could create objects that we could not destroy.
Prior to the new standard, classes prevented copies by declaring their copy constructor and copy-assignment operator as private. However, friends and members of the class can still make copies. To prevent these copies, we declare these members as private but do not define them. An attempt to use an undefined member results in a link-time failure. Classes that want to prevent copying should define their copy constructor and copy-assignment operator using = delete rather than making those members private.
Classes That Act Like Values
To decide what copying an object of type mean, we have two choices: We can define the copy operations to make the class behave like a value or like a pointer. Classes that behave like values have their own state. Classes that act like pointers share state. To illustrate these two approaches, we’ll make a class act like a value, then like a pointer.
To provide valuelike behavior, each object has to have its own copy of the resource that the class manages. There are two points to keep in mind when you write an assignment operator:
- assignment operator must work correctly if an object is assigned to itself
- most assignment operators share work with the destructor and copy constructor
A good pattern to use is to first copy the right-hand operand into a local temporary. After the copy is done, it is safe to destroy the existing members of the left-hand operand.
|
|
Classes That Act Like Pointers
The easiest way to make a class act like a pointer is to use shared_ptr to manage the resource in the class. Copying (assigning) a shared_ptr copies (assigns) the pointer to which the shared_ptr points. Sometimes we want to manage a resource directly, so we will do our own reference counting. Reference counting works as follows:
- each constructor (other than copy constructor) creates a counter that keep track of how many objects share state
- copy constructor does not allocate a new counter, it just copies data members , including the counter. Then the copy constructor increments this shared counter, indicating that there is another user of that object’s state.
- the destructor decrements the counter. If the count goes to zero, the destructor deletes that state.
- copy assignment operator increments the right-hand operand’s counter and decrements the counter of the left-hand operand. If the counter for the left-hand operand goes to zero, we must destroy the state of the left-hand operand.
So where to put the reference count ? One way to solve this problem is to store the counter in dynamic memory.
|
|
Swap
In addition to defining the copy-control members, classes that manage resources often also define a function named swap. If a class defines its own swap, then the algorithm which need to exchange two elements uses that class-specific version. Otherwise, it uses the swap function defined by the library. The swap function involves a copy and two assignments:
|
|
In principle, none of the memory allocation is necessary. Rather than allocating new copies of the string, we’d like swap to swap the pointers. We can override the default behavior of swap by defining a version of swap that operate on our class.
|
|
Classes that define swap often use swap to define their assignment operator. These operators use a technique known as copy and swap. This technique swaps the left-hand operand with a copy of the right-hand operand:
|
|
In this version of the assignment operator, the parameter is not a reference. Instead, we pass the right-hand operand by value. Copying a HasPtr allocates a new copy of that object’s string. When it finishes, rhs is destroyed and the destructor deletes the memory to which rhs now points. The interesting thing about this technique is that it automatically handles self assignment and is automatically exception safe.
Classes That Manage Dynamic Memory
Some classes need to allocate a varying amount of storage at run time. Such classes often can use a library container to hold their data. However, this strategy does not work for every class; some classes need to do their own allocation. Such classes generally must define their own copy-control members to manage the memory they allocate. As an example, we’ll implement a simplification of the library vector
In our StrVec class, We’ll use an allocator to obtain raw memory. Then we can use the allocator’s construct member to create objects in that space when we need to add an element, and use the destroy member to remove an element. Each StrVec will have three pointers into the space it uses for its elements:
- elements, which points to the first element in the allocated memory
- first_free, which points just after the last actual element
- cap, which points just past the end of the allocated memory
In addition to these pointers, StrVec will have a member named alloc that is an allocator
alloc_n_copywill allocate space and copy a given range of elements.freewill destroy the constructed elements and deallocate the space.chk_n_allocwill ensure that there is room to add at least one more element to the StrVec. If there isn’t room for another element, chk_n_alloc will call reallocate to get more space.reallocatewill reallocate the StrVec when it runs out of space
Copying a string copies the data because ordinarily after we copy a string, there are two users of that string. However, when reallocate copies the strings in a StrVec, there will be only one user of these strings after the copy. So copying the data in these strings is unnecessary.
Under the new standard, we can avoid copying the strings by using two facilities. First, the library classes define “move constructors”. The move constructors operate by “moving” resources from the given object to the object being constructed. For example, the string move constructor copies the pointer rather than allocating space for copying the characters themselves. Second, we’ll use a library function named move, defined in utility header. There are two points about move:
- When reallocate constructs strings in the new memory, it must call move to signal that it wants to use the string move constructor. If it omits this call, the copy constructor will be used.
- We usually do not provide a using declaration for move. When we use move, we call std::move, not move.
|
|
Note: One of the major features in the new standard is the ability to move rather than copy an object. The library containers, string, and shared_ptr clases support move as well as copy. The IO and unique_ptr classes can be moved but not copied.
Rvalue References
To support move operations, the new standard introduced a new kind of reference, an rvalue reference. An rvalue reference is a reference that must be bound to an rvalue. An rvalue reference is obtained by using && rather than &. As we’ll see, rvalue references have the important property that they may be bound only to an object that is about to be destroyed. Generally speaking, an lvalue expression refers to an object’s identity whereas an rvalue expression refers to an object’s value.
|
|
Variables are lvalues. As a result, we cannot bind an rvalue reference to a variable defined as an rvalue reference type:
|
|
Alghough we cannot directly bind an rvalue reference to an lvalue, we can explicitly cast an lvalue to its corresponding rvalue reference type. We can also obtain an rvalue reference bound to an lvaue by calling a new library function named move, which is defined in the utility header. The move function uses facilities to return an rvalue reference to its given object.
|
|
Calling move tells the compiler that we have an lvalue that we want to treat as if it were an rvalue. It is essential to realize that the call to move promises that we do not intend to use rr1 again except to assign to it or to destroy it. After a call to move, we cannot make any assumptions about the value of the moved-from object. We can destroy a moved-from object and can assign a new value to it, but we cannot use the value of a moved-from object.
Code that uses move should use std::move, not move. Doing so avoids potential name collisions.
Move Constructor
To enable move operations for our own types, we define a move constructor and a move-assignment operator. These members are similar to the corresponding copy operations, but they “steal” resources from their given object rather than copy them.
Like the copy constructor, the move constructor has an initial parameter that is a reference to the class type. Differently from the copy constructor, the reference parameter in the move constructor is an rvalue reference. As in the copy constructor, any additional parameters must all have default arguments. In addition to moving resources, the move constructor must ensure that the moved-from object is left in a state such that destroying that object will be harmless. In particular, once its resources are moved, the original object must no longer point to those moved resources. Responsibility for those resources has been assumed by the newly created object.
|
|
Unlike the copy constructor, the move constructor does not allocate any new memory; it takes over the memory in the given object. As a result, move operations ordinarily will not throw any exceptions. As we’ll see, unless the library knows that our move constructor won’t throw, it will do extra work to cater to the possibliity that moving an object of our class type might throw. One way inform the library is to specify noexcept on our constructor. We specify noexcept on a function after its parameter list. Move constructors and move assignment operators that cannot throw exceptions should be marked as noexcept.
So why noexcept is needed ? The push_back operation in the class StrVec might require that the vector be reallocated. As we’ve just seen, moving an object generally changes the value of the moved-from object. If reallocation uses a move constructor which throw an exception after moving some but not all of elements, the moved-from elements in the old space would have been changed, and the unconstructed elements in the new space would not yet exist. In this case, vector would be unable to meet its requirement that the vector is left unchanged. On the other hand, if vector uses the copy constructor and an exception happens, it can easily meet this requirement. In this case, while the elements are being constructed in the new memory, the old elements remain unchanged. If we want objects of our type to be moved rather than copied in circumstances such as vector reallocation, we must explicity tell the library that our move constructor is safe to use by using noexcept.
Move-Assignment Operator
|
|
Moving from an object does not destroy that object: Sometime after the move operation completes, the moved-from object will be destroyed. Therefore, when we write a move operation, we must ensure that the moved-from object is in a state in which the destructor can be run. After a move operation, the moved-from object must remain a valid, destructible object but users may make no assumptions about its value.
The Synthesized Move Operations
Recall that if we do not declare our own copy constructor or copy-assignment operator the compiler always synthesizes these operations. The compiler will synthesize a move constructor or a move-assignment operator only if the class doesn’t define any of its own copy-control members and if every nonstatic data member of the class can be moved.
|
|
Move Operation Defined as Deleted
Unlike the copy operations, a move operation is never implicitly defined as a deleted function. However, if we explicitly ask the compiler to generate a move operation by using = default, and the compiler is unable to move all the members, then the move operation will be defined as deleted. The rules for when a synthesized move operation is defined as deleted:
- Unlike the copy constructor, the move constructor is defined as deleted if the class has a member that defines its own copy constructor but does not also define a move constructor, or if the class has a member that doesn’t define its own copy operations and for which the compiler is unable to synthesize a move constructor. Similarly for move-assignment.
- The move constructor or move-assignment operator is defined as deleted if the class has a member whose own move constructor or move-assignment operator is deleted or inaccessible.
- Like the copy constructor, the move constructor is defined as deleted if the destructor is deleted or inaccessible.
- Like the copy-assignment operator, the move-assignment operator is defined as deleted if the class has a const or reference member.
Note: Classes that define a move constructor or move-assignment operator must also define their own copy operations. Otherwise, those members are deleted by default.
Which Constructor to Use
When a class has both a move constructor and a copy constructor, the compiler uses ordinary function matching to determine which constructor to use. Similarly for assignment. If a class has no move constructor, function matching ensures that objects of that type are copied, even if we attempt to move them by calling move.
Copy-and-Swap Assignment Operators and Move
|
|
If we add a move constructor to this class, it will effectively get a move assignment operator as well. Now let’s look at the assignment operator. That operator has a nonreference parameter, which means the parameter is copy initialized. Copy initialization uses either the copy constructor or the move constructor; lvalues are copied and rvalues are moved. As a result, this single assignment operator acts as both the copy-assignment and move-assignment operator.
The third rule of thumb: All five copy-control members should be thought of as a unit. Ordinarily, if a class defines any of these operations, it usually should define them all.
Move Iterators
The reallocate member of StrVec used a for loop to call construct to copy the elements from the old memory to the new. As an alternative to writing that loop, it would be easier if we could call uninitialized_copy to construct the newly allocated space. However, uninitialized_copy does what it says: It copies the elements. There is no analogous library function to “move” objects into unconstructed memory.
Instead, the new library defines a move iterator adaptor. A move iterator adapts its given iterator by changing the behavior of the iterator’s dereference operator. Ordinarily, an iterator dereference operator returns an lvalue reference to the element. Unlike other iterators, the dereference operator of a move iterator yields an rvalue reference.
We transform an ordinary iterator to a move iterator by calling the library make_move_iterator function. This function takes an iterator and returns a move iterator. All of the original iterator’s other operations work as usual.
|
|
uninitialized_copy calls construct on each element in the input sequence to “copy” that element into the destination. That algorithm uses the iterator dereference operator to fetch elements from the input sequence. Because we passed move iterators, the dereference operator yields an rvalue reference, which means construct will use the move constructor to construct the elements. You should pass move iterators to algorithms only when you are confident that the algorithm does not access an element after it has assigned to that element or passed that element to a user-defined function.
Because a moved-from object has indeterminate state, calling std::move on an object is a dangerous operation. When we call move, we must be absolutely certain that there can be no other users of the moved-from object.
Rvalue References and Member Functions
Member functions other than constructors and assignment can benefit from providing both copy and move versions. For example, the library containers that define push_back provide two versions:
|
|
Overloaded functions that distinguish between moving and copying a parameter typically have one version that takes a const T& and one that takes a T&&. Ordinarily, there is no need to define versions of the operation that take a const X&& or a (plain) X&. Usually, we pass an rvalue reference when we want to “steal” from the argument. In order to do so, the argument must not be const. Similarly, copying from an object should not change the object being copied.
|
|
The difference is that the rvalue reference version of push_back calls move to pass its parameter to construct. As we’ve seen, the construct function uses the type of its second and subsequent arguments to determine which constructor to use.
|
|
When we call push_back the type of the argument determines whether the new element is copied or moved into the container. These calls differ as to whether the argument is an lvalue (s) or an rvalue (the temporary string created from “done”).
The reference qualifier
The following usage can be surprising when we assign to the rvalue. Prior to the new standard, there was no way to prevent such usage. In order to maintain backward compatability, the library classes continue to allow assignment to rvalues, However, we might want to prevent such usage in our own classes. In this case, we’d like to force the left-hand operand to be an lvalue.
|
|
C++ 11, We indicate the lvalue/rvalue property of “this” in the same way that we define const member functions; we place a reference qualifier after the parameter list.
|
|
The reference qualifier can be either & or &&, indicating that “this” may point to an lvalue or rvalue, respectively. Like the const qualifier, a reference qualifier may appear only on a (nonstatic) member function and must appear in both the declaration and definition of the function.
A function can be both const and reference qualified. In such cases, the reference qualifier must follow the const qualifier:
|
|
Overloading and Reference Functions
Just as we can overload a member function based on whether it is const, we can also overload a function based on its reference qualifier.
|
|
The object is an rvalue, which means it has no other users, so we can change the object itself. When we run sorted on a const rvalue or on an lvalue, we can’t change this object, so we copy data before sorting it. Overload resolution uses the lvalue/rvalue property of the object that calls sorted to determine which version is used:
|
|
When we define two or more members that have the same name and the same parameter list, we must provide a reference qualifier on all or none of those functions:
|
|
Overloaded Operations and Conversions
C++ lets us define what the operators mean when applied to objects of class type. It also lets us define conversions for class types. Class-type conversions are used like the built-in conversions to implicitly convert an object of one type to another type when needed. Operator overloading lets us define the meaning of an operator when applied to operand(s) of a class type. Judicious use of operator overloading can make our programs easier to write and easier to read.
Basic Concepts
Overloaded operators are functions with special names: the keyword operator followed by the symbol for the operator being defined. An overloaded operator function has the same number of parameters as the operator has operands. When an overloaded operator is a member function, this is bound to the left-hand operand. Member operator functions have one less (explicit) parameter than the number of operands.
We cannot change the meaning of an operator when applied to operands of built-in type. We can overload only existing operators and cannot invent new operator symbols. For example, we cannot define operator** to provide exponentiation. Four symbols (+, -, *, and &) serve as both unary and binary operators. Either or both of these operators can be overloaded. An overloaded operator has the same precedence and associativity as the corresponding built-in operator.
Calling Overloaded Operator Directly
|
|
These calls are equivalent: Both call the nonmember function operator+, passing data1 as the first argument and data2 as the second. And we call a member operator function explicitly in the same way that we call any other member function.
|
|
Some Operators Shouldn’t Be Overloaded
Because the overloaded versions of these operators do not preserve order of evaluation and/or short-circuit evaluation, it is usually a bad idea to overload them. Ordinarily, the comma, address-of, logical AND , and logical OR operators should not be overloaded.
Use Definitions That Are Consistent with the Built-in Meaning
- If the class does IO, define the shift operators to be consistent with how IO is done for the built-in types.
- If the class has an operation to test for equality, define operator==. If the class has operator==, it should usually have operator!= as well.
- If the class has a single, natural ordering operation, define operator<. If the class has operator<, it should probably have all of the relational operators.
- The return type of an overloaded operator usually should be compatible with the return from the built-in version of the operator.
Operator overloading is most useful when there is a logical mapping of a built-in operator to an operation on our type.
Choosing Member or Nonmember
When we define an overloaded operator, we must decide whether to make the operator a class member or an ordinary nonmember function.
- The assignment (=), subscript ([]), call (()), and member access arrow (->) operators must be defined as members.
- The compound-assignment operators ordinarily ought to be members. However, unlike assignment, they are not required to be members.
- Operators that change the state of their object or that are closely tied to their given type—such as increment, decrement, and dereference—usually should be members.
- Symmetric operators—those that might convert either operand, such as the arithmetic, equality, relational, and bitwise operators—usually should be defined as ordinary nonmember functions.
Programmers expect to be able to use symmetric operators in expressions with mixed types. For example, we can add an int and a double. The addition is symmetric because we can use either type as the left-hand or the right-hand operand. If we want to provide similar mixed-type expressions involving class objects, then the operator must be defined as a nonmember function. When we define an operator as a member function, then the left-hand operand must be an object of the class of which that operator is a member. For example:
|
|
If operator+ were a member of the string class, the first addition would be equivalent to s.operator+("!"). Likewise, “hi” + s would be equivalent to “hi”.operator+(s). However, the type of “hi” is const char*, and that is a built-in type; it does not even have member functions.
Overloading the Output Operator
Ordinarily, the first parameter of an output operator is a reference to a nonconst ostream object. The ostream is nonconst because writing to the stream changes its state. The parameter is a reference because we cannot copy an ostream object. The second parameter ordinarily should be a reference to const of the class type we want to print. The parameter is a reference to avoid copying the argument. It can be const because (ordinarily) printing an object does not change that object. To be consistent with other output operators, operator« normally returns its ostream parameter.
|
|
Generally, output operators should print the contents of the object, with minimal formatting. They should not print a newline.
Input and output operators that conform to the conventions of the iostream library must be ordinary nonmember functions.
Overloading the Input Operator
Ordinarily the first parameter of an input operator is a reference to the stream from which it is to read, and the second parameter is a reference to the (nonconst) object into which to read. The operator usually returns a reference to its given stream.
|
|
Input operators must deal with the possibility that the input might fail; output operators generally don’t bother. Some input operators need to do additional data verification. In such cases, the input operator might need to set the stream’s condition state to indicate failure, even though technically speaking the actual IO was successful. Usually an input operator should set only the failbit. Setting eofbit would imply that the file was exhausted, and setting badbit would indicate that the stream was corrupted. These errors are best left to the IO library itself to indicate.
Arithmetic and Relational Operators
Ordinarily, we define the arithmetic and relational operators as nonmember functions in order to allow conversions for either the left- or right-hand operand.
|
|
Classes that define both an arithmetic operator and the related compound assignment ordinarily ought to implement the arithmetic operator by using the compound assignment.
|
|
Classes for which there is a logical meaning for equality normally should define operator==. Classes that define == make it easier for users to use the class with the library algorithms.
For Sales_data, there is no single logical definition of <. Thus, it is better for this class not to define < at all. If a single logical definition for < exists, classes usually should define the < operator. However, if the class also has ==, define < only if the definitions of < and == yield consistent results.
Assignment Operators
In addition to the copy- and move-assignment operators that assign one object of the class type to another object of the same type, a class can define additional assignment operators that allow other types as the right-hand operand. As one example, the library vector class defines a third assignment operator that takes a braced list of elements. We can add this operator to our StrVec class:
|
|
Assignment operators can be overloaded. Assignment operators, regardless of parameter type, must be defined as member functions.
Compound-Assignment Operators
Compound assignment operators are not required to be members. However, we prefer to define all assignments, including compound assignments, in the class. For example:
|
|
Assignment operators must, and ordinarily compound-assignment operators should, be defined as members. These operators should return a reference to the left-hand operand.
Subscript Operator
Classes that represent containers from which elements can be retrieved by position often define the subscript operator, operator[]. The subscript operator must be a member function. If a class has a subscript operator, it usually should define two versions: one that returns a plain reference and the other that is a const member and returns a reference to const.
|
|
Increment and Decrement Operators
The increment (++) and decrement (–) operators are most often implemented for iterator classes. These operators let the class move between the elements of a sequence. Classes that define increment or decrement operators should define both the prefix and postfix versions. These operators usually should be defined as members.
|
|
To be consistent with the built-in operators, the prefix operators should return a reference to the incremented or decremented object.
|
|
There is one problem with defining both the prefix and postfix operators: Normal overloading cannot distinguish between these operators. To solve this problem, the postfix versions take an extra (unused) parameter of type int.
|
|
To be consistent with the built-in operators, the postfix operators should return the old (unincremented or undecremented) value. That value is returned as a value, not a reference. The postfix versions have to remember the current state of the object before incrementing the object:
|
|
The int parameter is not used, so we do not give it a name. If we want to call the postfix version using a function call, then we must pass a value for the integer argument:
|
|
Member Access Operators
The dereference (*) and arrow (->) operators are often used in classes that represent iterators and in smart pointer classes.
|
|
Operator arrow must be a member. The dereference operator is not required to be a member but usually should be a member as well. The overloaded arrow operator must return either a pointer to a class type or an object of a class type that defines its own operator arrow.
Function-Call Operator
Classes that overload the call operator allow objects of its type to be used as if they were a function. Because such classes can also store state, they can be more flexible than ordinary functions.
|
|
We use the call operator by applying an argument list to an absInt object in a way that looks like a function call. Even though absObj is an object, not a function, we can “call” this object. Calling an object runs its overloaded call operator.
|
|
The function-call operator must be a member function. A class may define multiple versions of the call operator, each of which must differ as to the number or types of their parameters. Objects of classes that define the call operator are referred to as function objects. Such objects “act like functions” because we can call them.
As an example, we’ll define a class that prints a string argument.
|
|
When we define PrintString objects, we can use the defaults or supply our own values for the separator or output stream:
|
|
Function objects are most often used as arguments to the generic algorithms. For example, we can use the library for_each algorithm and our PrintString class to print the contents of a container:
|
|
Lambdas Are Function Objects
In the previous section, we used a PrintString object as an argument in a call to for_each. This usage is similar to the programs that used lambda expressions. When we write a lambda, the compiler translates that expression into an unnamed object of an unnamed class. The classes generated from a lambda contain an overloaded function-call operator. For example, the lambda that we passed as the last argument to stable_sort:
|
|
acts like an unnamed object of a class that would look something like
|
|
By default, lambdas may not change their captured variables. As a result, by default, the function-call operator in a class generated from a lambda is a const member function. If the lambda is declared as mutable, then the call operator is not const. We can rewrite the call to stable_sort to use this class instead of the lambda expression:
|
|
As we’ve seen, when a lambda captures a variable by reference, the compiler is permitted to use the reference directly without storing that reference as a data member in the generated class. In contrast, variables that are captured by value are copied into the lambda. As a result, classes generated from lambdas that capture variables by value have data members corresponding to each such variable. As an example, the lambda that we used to find the first string whose length was greater than or equal to a given bound:
|
|
would generate a class that looks something like
|
|
Unlike our ShorterString class, this class has a data member and a constructor to initialize that member. This synthesized class does not have a default constructor; to use this class, we must pass an argument:
|
|
Classes generated from a lambda expression have a deleted default constructor, deleted assignment operators, and a default destructor. Whether the class has a defaulted or deleted copy/move constructor depends in the usual ways on the types of the captured data members.
Library-Defined Function Objects
The standard library defines a set of classes that represent the arithmetic, relational, and logical operators. Each class defines a call operator that applies the named operation. These classes are templates to which we supply a single type. That type specifies the parameter type for the call operator. For example, plus
|
|
These types are defined in the functional header.
|
|
The function-object classes that represent operators are often used to override the default operator used by an algorithm. As we’ve seen, by default, the sorting algorithms use operator<, which ordinarily sorts the sequence into ascending order. To sort into descending order, we can pass an object of type greater.
|
|
One important aspect of these library function objects is that the library guarantees that they will work for pointers. Recall that comparing two unrelated pointers is undefined. However, we might want to sort a vector of pointers based on their addresses in memory.
|
|
Callable Objects and function
C++ has several kinds of callable objects: functions and pointers to functions, lambdas, objects created by bind, and classes that overload the function-call operator. Sometimes we want to treat several callable objects that share a call signature as if they had the same type. For example, consider the following different types of callable objects:
|
|
Even though each has a distinct type, they all share the same call signature:
|
|
We might want to use these callables to build a simple desk calculator. To do so, we’d want to define a function table to store “pointers” to these callables. In C++, function tables are easy to implement using a map.
|
|
We could put a pointer to add into binops as follows:
|
|
However, we can’t store mod or div in binops:
|
|
The problem is that mod is a lambda, and each lambda has its own class type. That type does not match the type of the values stored in binops. We can solve this problem using a new library type named function that is defined in the functional header. function is a template. We can declare a function type that can represent callable objects that return an int result and have two int parameters.
|
|
We can now redefine our map using this function type:
|
|
We can add each of our callable objects, be they function pointers, lambdas, or function objects, to this map:
|
|
When we index binops, we get a reference to an object of type function. The function type overloads the call operator. That call operator takes its own arguments and passes them along to its stored callable object:
|
|
The function class in the new library is not related to classes named unary_function and binary_function that were part of earlier versions of the library. These classes have been deprecated by the more general bind function.
Conversion Operators
Converting constructors and conversion operators define class-type conversions. Such conversions are also referred to as user-defined conversions. A conversion operator is a special kind of member function that converts a value of a class type to a value of some other type. A conversion function typically has the general form
|
|
where type represents a type. Conversion operators can be defined for any type (other than void) that can be a function return type. Conversions to an array or a function type are not permitted. Conversions to pointer types—both data and function pointers—and to reference types are allowed.
Conversion operators have no explicitly stated return type and no parameters, and they must be defined as member functions. Conversion operations ordinarily should not change the object they are converting. As a result, conversion operators usually should be defined as const members.
|
|
The conversion operator converts SmallInt objects to int:
|
|
Although the compiler will apply only one user-defined conversion at a time, an implicit user-defined conversion can be preceded or followed by a standard (built-in) conversion. As a result, we can pass any arithmetic type to the SmallInt constructor.
|
|
Because conversion operators are implicitly applied, there is no way to pass arguments to these functions. Hence, conversion operators may not be defined to take parameters. Although a conversion function does not specify a return type, each conversion function must return a value of its corresponding type:
|
|
Conversion operators are misleading when there is no obvious single mapping between the class type and the conversion type. For example, consider a class that represents a Date. We might think it would be a good idea to provide a conversion from Date to int. Alternatively, the conversion operator might return an int representing the number of days that have elapsed since some epoch point, such as January 1, 1970. The problem is that there is no single one-to-one mapping between an object of type Date and a value of type int. In such cases, it is better not to define the conversion operator. Instead, the class ought to define one or more ordinary members to extract the information in these various forms.
explicit Conversion Operators
In practice, classes rarely provide conversion operators. It is not uncommon for classes to define conversions to bool.
|
|
This program attempts to use the output operator on an input stream. There is no « defined for istream, so the code is almost surely in error. However, this code could use the bool conversion operator to convert cin to bool. The resulting bool value would then be promoted to int and used as the left-hand operand to the built-in version of the left-shift operator. To prevent such problems, the new standard introduced explicit conversion operators:
|
|
If the conversion operator is explicit, we can still do the conversion. However, with one exception, we must do so explicitly through a cast.
|
|
The exception is that the compiler will apply an explicit conversion to an expression used as a condition. That is, an explicit conversion will be used implicitly to convert an expression used as
- The condition of an if, while, or do statement
- The condition expression in a for statement header
- An operand to the logical NOT (!), OR (||), or AND (&&) operators
- The condition expression in a conditional (?:) operator
Conversion to bool
Under the new standard, the IO library defines an explicit conversion to bool. Whenever we use a stream object in a condition, we use the operator bool that is defined for the IO types.
|
|
The condition in the while executes the input operator, which reads into value and returns cin. To evaluate the condition, cin is implicitly converted by the istream operator bool conversion function. That function returns true if the condition state of cin is good, and false otherwise. Conversion to bool is usually intended for use in conditions. As a result, operator bool ordinarily should be defined as explicit.
Avoiding Ambiguous Conversions
If a class has one or more conversions, it is important to ensure that there is only one way to convert from the class type to the target type. If there is more than one way to perform a conversion, it will be hard to write unambiguous code. There are two ways that multiple conversion paths can occur. The first happens when two classes provide mutual conversions. For example, mutual conversions exist when a class A defines a converting constructor that takes an object of class B and B itself defines a conversion operator to type A. The second way to generate multiple conversion paths is to define multiple conversions from or to types that are themselves related by conversions. The most obvious instance is the built-in arithmetic types. A given class ordinarily ought to define at most one conversion to or from an arithmetic type. So, ordinarily it is a bad idea to define classes with mutual conversions or to define conversions to or from two arithmetic types.
In the following example, we’ve defined two ways to obtain an A from a B: either by using B’s conversion operator or by using the A constructor that takes a B:
|
|
Because there are two ways to obtain an A from a B, the compiler doesn’t know which conversion to run; the call to f is ambiguous. If we want to make this call, we have to explicitly call the conversion operator or the constructor:
|
|
The following class has converting constructors from two different arithmetic types, and conversion operators to two different arithmetic types:
|
|
In the call to f2, neither conversion is an exact match to long double. However, the call is ambiguous. We encounter the same problem when we try to initialize a2 from a long. Neither constructor is an exact match for long. The call to f2, and the initialization of a2, are ambiguous because the standard conversions that were needed had the same rank.
|
|
When two user-defined conversions are used, the rank of the standard conversion, if any, preceding or following the conversion function is used to select the best match.
Overloaded Functions and Converting Constructors
Choosing among multiple conversions is further complicated when we call an overloaded function. If two or more conversions provide a viable match, then the conversions are considered equally good.
|
|
Here both C and D have constructors that take an int. Hence, the call is ambiguous. The caller can disambiguate by explicitly constructing the correct type:
|
|
Needing to use a constructor or a cast to convert an argument in a call to an overloaded function frequently is a sign of bad design.
Overloaded Functions and User-Defined Conversion
In a call to an overloaded function, if two (or more) user-defined conversions provide a viable match, the conversions are considered equally good. The rank of any standard conversions that might or might not be required is not considered.
|
|
In this case, C has a conversion from int and E has a conversion from double. For the call manip2(10), both manip2 functions are viable. Because calls to the overloaded functions require different user-defined conversions from one another, this call is ambiguous. In particular, even though one of the calls requires a standard conversion and the other is an exact match, the compiler will still flag this call as an error.
Function Matching and Overloaded Operators
Overloaded operators are overloaded functions. Normal function matching is used to determine which operator—built-in or overloaded—to apply to a given expression. However, when an operator function is used in an expression, the set of candidate functions is broader than when we call a function using the call operator. If a has a class type, the expression a sym b might be
|
|
When a call is through an object of a class type (or through a reference or pointer to such an object), then only the member functions of that class are considered. When we use an overloaded operator in an expression, there is nothing to indicate whether we’re using a member or nonmember function. Therefore, the set of candidate functions for an operator used in an expression can contain both nonmember and member functions. As an example, we’ll define an addition operator for our SmallInt class:
|
|
We can use this class to add two SmallInts, but we will run into ambiguity problems if we attempt to perform mixed-mode arithmetic:
|
|
The first addition uses the overloaded version of + that takes two SmallInt values. The second addition is ambiguous, because we can convert 0 to a SmallInt and use the SmallInt version of +, or convert s3 to int and use the built-in addition operator on ints. Providing both conversion functions to an arithmetic type and overloaded operators for the same class type may lead to ambiguities between the overloaded operators and the built-in operators.
Object-Oriented Programming
The key ideas in object-oriented programming are data abstraction, inheritance, and dynamic binding. Using data abstraction, we can define classes that separate interface from implementation. Through inheritance, we can define classes that model the relationships among similar types. Through dynamic binding, we can use objects of these types while ignoring the details of how they differ.
Inheritance
Classes related by inheritance form a hierarchy. Typically there is a base class at the root of the hierarchy, from which the other classes inherit, directly or indirectly. These inheriting classes are known as derived classes. The base class defines those members that are common to the types in the hierarchy. Each derived class defines those members that are specific to the derived class itself.
To model our different kinds of pricing strategies, we’ll define a class named Quote, which will be the base class of our hierarchy. A Quote object will represent undiscounted books. From Quote we will inherit a second class, named Bulk_quote, to represent books that can be sold with a quantity discount. In C++, a base class distinguishes functions that are type dependent from those that it expects its derived classes to inherit without change. The base class defines as virtual those functions it expects its derived classes to define for themselves.
|
|
Because Bulk_quote uses public in its derivation list, we can use objects of type Bulk_quote as if they were Quote objects. A derived class must include in its own class body a declaration of all the virtual functions it intends to define for itself. A derived class may include the virtual keyword on these functions but is not required to do so. The new standard lets a derived class explicitly note that it intends a member function to override a virtual that it inherits. It does so by specifying override after its parameter list.
Dynamic Binding
Through dynamic binding, we can use the same code to process objects of either type Quote or Bulk_quote interchangeably.
|
|
Because the parameter is a reference to Quote, we can call this function on either a Quote object or a Bulk_quote object. Because net_price is a virtual function, and because print_total calls net_price through a reference, the version of net_price that is run will depend on the type of the object that we pass to print_total:
|
|
Because the decision as to which version to run depends on the type of the argument, that decision can’t be made until run time. Therefore, dynamic binding is sometimes known as run-time binding. In C++, dynamic binding happens when a virtual function is called through a reference (or a pointer) to a base class.
Defining a Base Class
|
|
Base classes ordinarily should define a virtual destructor. Virtual destructors are needed even if they do no work.
In C++, a base class must distinguish the functions it expects its derived classes to override from those that it expects its derived classes to inherit without change. The base class defines as virtual those functions it expects its derived classes to override. When we call a virtual function through a pointer or reference , the call will be dynamically bound. Any nonstatic member function, other than a constructor, may be virtual. The virtual keyword appears only on the declaration inside the class and may not be used on a function definition that appears outside the class body.
A derived class inherits the members defined in its base class. Like any other code that uses the base class, a derived class may access the public members of its base class but may not access the private members. However, sometimes a base class has members that it wants to let its derived classes use while still prohibiting access to those same members by other users. We specify such members after a protected access specifier.
Defining a Derived Class
|
|
When the derivation is public, the public members of the base class become part of the interface of the derived class as well.
If a derived class does not override a virtual from its base, then, like any other member, the derived class inherits the version defined in its base class. A derived class may include the virtual keyword on the functions it overrides, but it is not required to do so. The new standard lets a derived class explicitly note that it intends a member function to override a virtual that it inherits. It does so by specifying override after the parameter list, or after the const or reference qualifier(s) if the member is a const or reference function.
A derived object contains multiple parts: a subobject containing the (nonstatic) members defined in the derived class itself, plus subobjects corresponding to each base class from which the derived class inherits. Thus, a Bulk_quote object will contain four data elements: the bookNo and price data members that it inherits from Quote, and the min_qty and discount members, which are defined by Bulk_quote. The base and derived parts of an object are not guaranteed to be stored contiguously. Because a derived object contains subparts corresponding to its base class(es), we can use an object of a derived type as if it were an object of its base type.
|
|
This conversion is often referred to as the derived-to-base conversion. The fact that the derived-to-base conversion is implicit means that we can use an object of derived type or a reference to a derived type when a reference to the base type is required. Similarly, we can use a pointer to a derived type where a pointer to the base type is required.
Derived-Class Constructors
Although a derived object contains members that it inherits from its base, it cannot directly initialize those members. Like any other code that creates an object of the base-class type, a derived class must use a base-class constructor to initialize its base-class part. The base-class part of an object is initialized, along with the data members of the derived class, during the initialization phase of the constructor. Analogously to how we initialize a member, a derived-class constructor uses its constructor initializer list to pass arguments to a base-class constructor. For example, the Bulk_quote constructor with four parameters:
|
|
As with a data member, unless we say otherwise, the base part of a derived object is default initialized. To use a different base-class constructor, we provide a constructor initializer using the name of the base class. The base class is initialized first, and then the members of the derived class are initialized in the order in which they are declared in the class.
A derived class may access the public and protected members of its base class:
|
|
It is essential to understand that each class defines its own interface. Interactions with an object of a class-type should use the interface of that class, even if that object is the base-class part of a derived object. As a result, derived-class constructors may not directly initialize the members of its base class. The constructor body of a derived constructor can assign values to its public or protected base-class members. Although it can assign to those members, it generally should not do so. Like any other user of the base class, a derived class should respect the interface of its base class by using a constructor to initialize its inherited members.
Inheritance and static Members
|
|
static members obey normal access control. If the member is private in the base class, then derived classes have no access to it.
|
|
Classes Used as a Base Class
A class must be defined, not just declared, before we can use it as a base class:
|
|
A base class can itself be a derived class:
|
|
In this hierarchy, Base is a direct base to D1 and an indirect base to D2. A direct base class is named in the derivation list. Effectively, the most derived object contains a subobject for its direct base and for each of its indirect bases.
Preventing Inheritance
Under the new standard, we can prevent a class from being used as a base by following the class name with final:
|
|
Conversions and Inheritance
Ordinarily, we can bind a reference or a pointer only to an object that has the same type as the corresponding reference or pointer or to a type that involves an acceptable const conversion. Classes related by inheritance are an important exception: We can bind a pointer or reference to a base-class type to an object of a type derived from that base class. For example, we can use a Quote& to refer to a Bulk_quote object, and we can assign the address of a Bulk_quote object to a Quote*.
The fact that we can bind a reference (or pointer) to a base-class type to a derived object has a crucially important implication: When we use a reference (or pointer) to a base-class type, we don’t know the actual type of the object to which the pointer or reference is bound. That object can be an object of the base class or it can be an object of a derived class.
There Is No Implicit Conversion from Base to Derived. We cannot convert from base to derived even when a base pointer or reference is bound to a derived object:
|
|
If the base class has one or more virtual functions, we can use a dynamic_cast to request a conversion that is checked at run time. Alternatively, in those caseswhen we know that the conversion from base to derived is safe, we can use a static_cast to override the compiler.
The automatic derived-to-base conversion applies only for conversions to a reference or pointer type. There is no such conversion from a derived-class type to the base-class type.
|
|
When item is constructed, the Quote copy constructor is run. That constructor knows only about the bookNo and price members. It copies those members from the Quote part of bulk and ignores the members that are part of the Bulk_quote portion of bulk. Because the Bulk_quote part is ignored, we say that the Bulk_quote portion of bulk is sliced down. When we initialize or assign an object of a base type from an object of a derived type, only the base-class part of the derived object is copied, moved, or assigned. The derived part of the object is ignored.
Virtual Functions
|
|
It is crucial to understand that dynamic binding happens only when a virtual function is called through a pointer or a reference.
|
|
When we call a virtual function on an expression that has a plain—nonreference and nonpointer—type, that call is bound at compile time. For example, We can change the value of the object that base represents, but there is no way to change the type of that object. Hence, this call is resolved, at compile time, to the Quote version of net_price.
The key idea behind OOP is polymorphism. Polymorphism is derived from a Greek word meaning “many forms.” We speak of types related by inheritance as polymorphic types, because we can use the “many forms” of these types while ignoring the differences among them.
A function that is virtual in a base class is implicitly virtual in its derived classes. When a derived class overrides a virtual, the parameters in the base and derived classes must match exactly.
It is legal for a derived class to define a function with the same name as a virtual in its base class but with a different parameter list. In practice, such declarations often are a mistake—the class author intended to override a virtual from the base class but made a mistake in specifying the parameter list. Under the new standard we can specify override on a virtual function in a derived class.
|
|
We can also designate a function as final. Any attempt to override a function that has been defined as final will be flagged as an error:
|
|
Like any other function, a virtual function can have default arguments. If a call uses a default argument, the value that is used is the one defined by the static type through which the function is called. That is, when a call is made through a reference or pointer to base, the default argument(s) will be those defined in the base class. The base-class arguments will be used even when the derived version of the function is run.
In some cases, we want to prevent dynamic binding of a call to a virtual function; we want to force the call to use a particular version of that virtual. We can use the scope operator to do so. For example, this code:
|
|
Ordinarily, only code inside member functions (or friends) should need to use the scope operator to circumvent the virtual mechanism. Why might we wish to circumvent the virtual mechanism? The most common reason is when a derived-class virtual function calls the version from the base class. In such cases, the base-class version might do work common to all types in the hierarchy. The versions defined in the derived classes would do whatever additional work is particular to their own type. If a derived virtual function that intended to call its base-class version omits the scope operator, the call will be resolved at run time as a call to the derived version itself, resulting in an infinite recursion.
Abstract Base Classes
In practice, we’d like to prevent users from creating Disc_quote objects at all. This class represents the general concept of a discounted book, not a concrete discount strategy. We can enforce this design intent, by defining net_price as a pure virtual function. Unlike ordinary virtuals, a pure virtual function does not have to be defined. It is worth noting that we can provide a definition for a pure virtual. However, the function body must be defined outside the class.
|
|
A class containing (or inheriting without overridding) a pure virtual function is an abstract base class. An abstract base class defines an interface for subsequent classes to override. We cannot (directly) create objects of a type that is an abstract base class. Classes that inherit from Disc_quote must define net_price or those classes will be abstract as well.
Now we can reimplement Bulk_quote to inherit from Disc_quote rather than inheriting directly from Quote:
|
|
This version of Bulk_quote has a direct base class, Disc_quote, and an indirect base class, Quote. Each Bulk_quote object has three subobjects: an (empty) Bulk_quote part, a Disc_quote subobject, and a Quote subobject. As we’ve seen, each class controls the initialization of objects of its type. Therefore, even though Bulk_quote has no data members of its own, it provides the same four-argument constructor as in our original class.
Access Control and Inheritance
As we’ve seen, a class uses protected for those members that it is willing to share with its derived classes but wants to protect from general access. The protected specifier can be thought of as a blend of private and public:
- Like private, protected members are inaccessible to users of the class.
- Like public, protected members are accessible to members and friends of classes derived from this class.
In addition, protected has another important property:
- A derived class member or friend may access the protected members of the base class only through a derived object. The derived class has no special access to the protected members of base-class objects.
To understand this last rule, consider the following example:
|
|
That function is just a friend of Sneaky.
Access to a member that a class inherits is controlled by a combination of the access specifier for that member in the base class, and the access specifier in the derivation list of the derived class. As an example, consider the following hierarchy:
|
|
The derivation access specifier has no effect on whether members (and friends) of a derived class may access the members of its own direct base class. Access to the members of a base class is controlled by the access specifiers in the base class itself. The purpose of the derivation access specifier is to control the access that users of the derived class—including other classes derived from the derived class—have to the members inherited from Base:
|
|
The derivation access specifier used by a derived class also controls access from classes that inherit from that derived class:
|
|
Had we defined another class, say, Prot_Derv, that used protected inheritance, the public members of Base would be protected members in that class. Users of Prot_Derv would have no access to pub_mem, but the members and friends of Prot_Derv could access that inherited member.
Accessibility of Derived-to-Base Conversion
Whether the derived-to-base conversion is accessible depends on which code is trying to use the conversion and may depend on the access specifier used in the derived class’ derivation. Assuming D inherits from B:
- User code may use the derived-to-base conversion only if D inherits publicly from B. User code may not use the conversion if D inherits from B using either protected or private.
- Member functions and friends of D can use the conversion to B regardless of how D inherits from B. The derived-to-base conversion to a direct base class is always accessible to members and friends of a derived class.
- Member functions and friends of classes derived from D may use the derived-to-base conversion if D inherits from B using either public or protected. Such code may not use the conversion if D inherits privately from B.
In the absence of inheritance, we can think of a class as having two different kinds of users: ordinary users and implementors. Ordinary users write code that uses objects of the class type; such code can access only the public (interface) members of the class. Implementors write the code contained in the members and friends of the class. The members and friends of the class can access both the public and private (implementation) sections.
Under inheritance, there is a third kind of user, namely, derived classes. A base class makes protected those parts of its implementation that it is willing to let its derived classes use. The protected members remain inaccessible to ordinary user code; private members remain inaccessible to derived classes and their friends.
Like any other class, a class that is used as a base class makes its interface members public . A class that is used as a base class may divide its implementation into those members that are accessible to derived classes and those that remain accessible only to the base class and its friends. An implementation member should be protected if it provides an operation or data that a derived class will need to use in its own implementation. Otherwise, implementation members should be private.
Friendship and Inheritance
Just as friendship is not transitive, friendship is also not inherited.
|
|
The fact that f3 is legal may seem surprising, but it follows directly from the notion that each class controls access to its own members. Pal is a friend of Base, so Pal can access the members of Base objects. That access includes access to Base objects that are embedded in an object of a type derived from Base.
Exempting Individual Members
Sometimes we need to change the access level of a name that a derived class inherits. We can do so by providing a using declaration:
|
|
Because Derived uses private inheritance, the inherited members, size and n, are (by default) private members of Derived. The using declarations adjust the accessibility of these members. Users of Derived can access the size member, and classes subsequently derived from Derived can access n.
A using declaration inside a class can name any accessible member of a direct or indirect base class. Access to a name specified in a using declaration depends on the access specifier preceding the using declaration. That is, if a using declaration appears in a private part of the class, that name is accessible to members and friends only. If the declaration is in a public section, the name is available to all users of the class. If the declaration is in a protected section, the name is accessible to the members, friends, and derived classes.
Default Inheritance Protection Levels
By default, a derived class defined with the class keyword has private inheritance; a derived class defined with struct has public inheritance:
|
|
The only differences are the default access specifier for members and the default derivation access specifier. There are no other distinctions.
Class Scope under Inheritance
Each class defines its own scope within which its members are defined. Under inheritance, the scope of a derived class is nested inside the scope of its base classes. If a name is unresolved within the scope of the derived class, the enclosing base-class scopes are searched for a definition of that name.
Name Lookup Happens at Compile Time. The static type of an object, reference, or pointer determines which members of that object are visible.
As usual, names defined in an inner scope (e.g., a derived class) hide uses of that name in the outer scope. Aside from overriding inherited virtual functions, a derived class usually should not reuse names defined in its base class.
As Usual, Name Lookup Happens before Type Checking. As we’ve seen, functions declared in an inner scope do not overload functions declared in an outer scope. As a result, functions defined in a derived class do not overload members defined in its base class(es). As in any other scope, if a member in a derived class has the same name as a base-class member, then the derived member hides the base-class member within the scope of the derived class. The base member is hidden even if the functions have different parameter lists:
|
|
Overriding Overloaded Functions
A derived class can override zero or more instances of the overloaded functions it inherits. If a derived class wants to make all the overloaded versions available through its type, then it must override all of them or none of them. Instead of overriding every base-class version that it inherits, a derived class can provide a using declaration for the overloaded member. A using declaration specifies only a name; it may not specify a parameter list. Thus, a using declaration for a base-class member function adds all the overloaded instances of that function to the scope of the derived class.
Virtual Destructors
The primary direct impact that inheritance has on copy control for a base class is that a base class generally should define a virtual destructor. As with any other function, we arrange to run the proper destructor by defining the destructor as virtual in the base class:
|
|
Like any other virtual, the virtual nature of the destructor is inherited. Thus, classes derived from Quote have virtual destructors, whether they use the synthesized destructor or define their own version. So long as the base class destructor is virtual, when we delete a pointer to base, the correct destructor will be run:
|
|
Executing delete on a pointer to base that points to a derived object has undefined behavior if the base’s destructor is not virtual.
Destructors for base classes are an important exception to the rule of thumb that if a class needs a destructor, it also needs copy and assignment.
The fact that a base class needs a virtual destructor has an important indirect impact on the definition of base and derived classes: If a class defines a destructor—even if it uses = default to use the synthesized version—the compiler will not synthesize a move operation for that class.
Synthesized Copy Control and Inheritance
The synthesized copy-control members in a base or a derived class execute like any other synthesized constructor, assignment operator, or destructor: They memberwise initialize, assign, or destroy the members of the class itself. In addition, these synthesized members initialize, assign, or destroy the direct base part of an object by using the corresponding operation from the base class.
It is worth noting that it doesn’t matter whether the base-class member is itself synthesized or has a an user-provided definition. All that matters is that the corresponding member is accessible and that it is not a deleted function.
The synthesized default constructor, or any of the copy-control members of either a base or a derived class, may be defined as deleted for the same reasons as in any other class. In addition, the way in which a base class is defined can cause a derived-class member to be defined as deleted:
-
If the default constructor, copy constructor, copy-assignment operator, or destructor in the base class is deleted or inaccessible, then the corresponding member in the derived class is defined as deleted, because the compiler can’t use the base-class member to construct, assign, or destroy the base-class part of the object.
-
If the base class has an inaccessible or deleted destructor, then the synthesized default and copy constructors in the derived classes are defined as deleted, because there is no way to destroy the base part of the derived object.
-
As usual, the compiler will not synthesize a deleted move operation. If we use = default to request a move operation, it will be a deleted function in the derived if the corresponding operation in the base is deleted or inaccessible, because the base class part cannot be moved. The move constructor will also be deleted if the base class destructor is deleted or inaccessible.
|
|
Because the copy constructor is defined, the compiler will not synthesize a move constructor for class B. If a class derived from B wanted to allow its objects to be copied or moved, that derived class would have to define its own versions of these constructors.
Move Operations and Inheritance
As we’ve seen, most base classes define a virtual destructor. As a result, by default, base classes generally do not get synthesized move operations. Moreover, by default, classes derived from a base class that doesn’t have move operations don’t get synthesized move operations either.
Because lack of a move operation in a base class suppresses synthesized move for its derived classes, base classes ordinarily should define the move operations if it is sensible to do so. Our Quote class can use the synthesized versions. However, Quote must define these members explicitly. Once it defines its move operations, it must also explicitly define the copy versions as well:
|
|
Now, Quote objects will be memberwise copied, moved, assigned, and destroyed. Moreover, classes derived from Quote will automatically obtain synthesized move operations as well, unless they have members that otherwise preclude move.
Derived-Class Copy-Control Members
When a derived class defines a copy or move operation, that operation is responsible for copying or moving the entire object, including base-class members. We ordinarily use the corresponding base-class constructor to initialize the base part of the object:
|
|
By default, the base-class default constructor initializes the base-class part of a derived object. If we want copy (or move) the base-class part, we must explicitly use the copy (or move) constructor for the base class in the derived’s constructor initializer list.
Derived-Class Assignment Operator
Like the copy and move constructors, a derived-class assignment operator, must assign its base part explicitly:
|
|
Derived-Class Destructor
Recall that the data members of an object are implicitly destroyed after the destructor body completes. Similarly, the base-class parts of an object are also implicitly destroyed. As a result, unlike the constructors and assignment operators, a derived destructor is responsible only for destroying the resources allocated by the derived class:
|
|
Objects are destroyed in the opposite order from which they are constructed: The derived destructor is run first, and then the base-class destructors are invoked, back up through the inheritance hierarchy.
Calls to Virtuals in Constructors and Destructors
As we’ve seen, the base-class part of a derived object is constructed first. While the base-class constructor is executing, the derived part of the object is uninitialized. Similarly, derived objects are destroyed in reverse order, so that when a base class destructor runs, the derived part has already been destroyed. As a result, while these base-class members are executing, the object is incomplete.
To accommodate this incompleteness, the compiler treats the object as if its type changes during construction or destruction. That is, while an object is being constructed it is treated as if it has the same class as the constructor; calls to virtual functions will be bound as if the object has the same type as the constructor itself.
To understand this behavior, consider what would happen if the derived-class version of a virtual was called from a base-class constructor. However, those members are uninitialized while a base constructor is running. If such access were allowed, the program would probably crash.
If a constructor or destructor calls a virtual, the version that is run is the one corresponding to the type of the constructor or destructor itself.
Inherited Constructors
Under the new standard, a derived class can reuse the constructors defined by its direct base class. If the derived class does not directly define these constructors, the compiler synthesizes them as usual. A derived class inherits its base-class constructors by providing a using declaration that names its (direct) base class. As an example, we can redefine our Bulk_quote class to inherit its constructors from Disc_quote:
|
|
Ordinarily, a using declaration only makes a name visible in the current scope. When applied to a constructor, a using declaration causes the compiler to generate code. The compiler generates a derived constructor corresponding to each constructor in the base. That is, for each constructor in the base class, the compiler generates a constructor in the derived class that has the same parameter list. These compiler-generated constructors have the form:
|
|
In our Bulk_quote class, the inherited constructor would be equivalent to:
|
|
Unlike using declarations for ordinary members, a constructor using declaration does not change the access level of the inherited constructor(s). Moreover, a using declaration can’t specify explicit or constexpr. If a constructor in the base is explicit or constexpr, the inherited constructor has the same property.
If a base-class constructor has default arguments, those arguments are not inherited. Instead, the derived class gets multiple inherited constructors in which each parameter with a default argument is successively omitted.
If a base class has several constructors, then with two exceptions, the derived class inherits each of the constructors from its base class. The first exception is that a derived class can inherit some constructors and define its own versions of other constructors. The second exception is that the default, copy, and move constructors are not inherited.
Containers and Inheritance
We cannot put objects of types related by inheritance directly into a container, because there is no way to define a container that holds elements of differing types.
|
|
The elements in basket are Quote objects. When we add a Bulk_quote object to the vector its derived part is ignored. Because derived objects are “sliced down” when assigned to a base-type object, containers and types related by inheritance do not mix well.
When we need a container that holds objects related by inheritance, we typically define the container to hold pointers (preferably smart pointers) to the base class.
|
|
Writing a Basket Class
|
|
The elements in our multiset are shared_ptrs and there is no less-than operator for shared_ptr. As a result, we must provide our own comparison operation to order the elements. Here, we define a private static member, named compare, that compares the isbns of the objects to which the shared_ptrs point. We initialize our multiset to use this comparison function through an in-class initializer:
|
|
The multiset member is named items, and we’re initializing items to use our compare function. decltype(compare)* in the template parameter specifies the type of the comparator. It doesn’t tell which function is to be used. So we need specify a compare function. A better approach might be to use a function class type; then the function call can be resolved at compile time, and a default-constructed object will do the right thing:
|
|
The member function total_receipt prints an itemized bill for the contents of the basket and returns the price for all the items in the basket:
|
|
We skip over all the elements that match the current key by calling upper_bound. Inside the for loop, we call print_total to print the details for each book in the basket. When we dereference iter, we get a shared_ptr that points to the object we want to print. To get that object, we must dereference that shared_ptr. Thus, **iter is a Quote object.
Users of Basket still have to deal with dynamic memory, because add_item takes a shared_ptr. As a result, users have to write code such as
|
|
Our next step will be to redefine add_item so that it takes a Quote object instead of a shared_ptr. This new version of add_item will handle the memory allocation so that our users no longer need to do so. We’ll define two versions, one that will copy its given object and the other that will move from it.
|
|
The only problem is that add_item doesn’t know what type to allocate. Somewhere there will be a new expression such as: new Quote(sale). Unfortunately, this expression allocates an object of type Quote and copies the Quote portion of sale. However, sale might refer to a Bulk_quote object, in which case, that object will be sliced down. We’ll solve this problem by giving our Quote classes a virtual member that allocates a copy of itself.
|
|
Because we have a copy and a move version of add_item, we defined lvalue and rvalue versions of clone. Each clone function allocates a new object of its own type. The const lvalue reference member copies itself into that newly allocated object; the rvalue reference member moves its own data. Using clone, it is easy to write our new versions of add_item:
|
|
Our clone function is also virtual. Whether the Quote or Bulk_quote function is run, depends on the dynamic type of sale. We bind a shared_ptr to that object and call insert to add this newly allocated object to items. Note that because shared_ptr supports the derived-to-base conversion, we can bind a shared_ptr<Quote> to a Bulk_quote*.
Templates and Generic Programming
When we write a generic program, we write the code in a way that is independent of any particular type. For example, the library provides a single, generic definition of each container, such as vector. Templates are the foundation of generic programming. A template is a blueprint or formula for creating classes or functions. When we use a generic type, such as vector, or a generic function, such as find, we supply the information needed to transform that blueprint into a specific class or function.
Function Templates
Rather than defining a new function for each type, we can define a function template. A function template is a formula from which we can generate type-specific versions of that function. The template version of compare looks like:
|
|
A template definition starts with the keyword template followed by a template parameter list, which is a comma-separated list of one or more template parameters bracketed by the less-than (<) and greater-than (>) tokens. Template parameters represent types or values used in the definition of a class or function. When we use a template, we specify—either implicitly or explicitly template argument(s) to bind to the template parameter(s). Our compare function declares one type parameter named T.
When we call a function template, the compiler (ordinarily) uses the arguments of the call to deduce the template argument(s) for us. That is, when we call compare, the compiler uses the type of the arguments to determine what type to bind to the template parameter T.
The compiler uses the deduced template parameter(s) to instantiate a specific version of the function for us. When the compiler instantiates a template, it creates a new “instance” of the template using the actual template argument(s) in place of the corresponding template parameter(s).
|
|
Our compare function has one template type parameter. In particular, a type parameter can be used to name the return type or a function parameter type, and for variable declarations or casts inside the function body:
|
|
Each type parameter must be preceded by the keyword class or typename:
|
|
These keywords have the same meaning and can be used interchangeably inside a template parameter list. A template parameter list can use both keywords:
|
|
Nontype Template Parameters
In addition to defining type parameters, we can define templates that take nontype parameters. A nontype parameter represents a value rather than a type. Nontype parameters are specified by using a specific type name instead of the class or typename keyword. When the template is instantiated, nontype parameters are replaced with a value supplied by the user or deduced by the compiler. These values must be constant expressions.
As an example, we can write a version of compare that will handle string literals. Because we’d like to be able to compare literals of different lengths, we’ll give our template two nontype parameters. The first template parameter will represent the size of the first array, and the second parameter will represent the size of the second array:
|
|
When we call this version of compare:
|
|
the compiler will use the size of the literals to instantiate a version of the template with the sizes substituted for N and M. Remembering that the compiler inserts a null terminator at the end of a string literal, the compiler will instantiate
|
|
A nontype parameter may be an integral type, or a pointer or (lvalue) reference to an object or to a function type. An argument bound to a nontype integral parameter must be a constant expression. Arguments bound to a pointer or reference nontype parameter must have static lifetime. We may not use an ordinary (nonstatic) local object or a dynamic object as a template argument for reference or pointer nontype template parameters. A pointer parameter can also be instantiated by nullptr or a zero-valued constant expression.
A template nontype parameter is a constant value inside the template definition. A nontype parameter can be used when constant expressions are required, for example, to specify the size of an array.
inline and constexpr Function Templates
The inline or constexpr specifier follows the template parameter list and precedes the return type:
|
|
Type-Independent Code
Simple though it is, our initial compare function illustrates two important principles for writing generic code:
- The function parameters in the template are references to const.
- The tests in the body use only < comparisons.
By making the function parameters references to const, we ensure that our function can be used on types that cannot be copied. Most types—including the built-in types and, except for unique_ptr and the IO types, all the library types we’ve used do allow copying. By writing the code using only the < operator, we reduce the requirements on types that can be used with our compare function. In fact, if we were truly concerned about type independence and portability, we probably should have defined our function using the less:
|
|
The problem with our original version is that if a user calls it with two pointers and those pointers do not point to the same array, then our code is undefined.
Template Compilation
The fact that code is generated only when we use a template (and not when we define it) affects how we organize our source code and when errors are detected. Ordinarily, when we call a function, the compiler needs to see only a declaration for the function. Similarly, when we use objects of class type, the class definition must be available, but the definitions of the member functions need not be present. As a result, we put class definitions and function declarations in header files and definitions of ordinary and class-member functions in source files.
Templates are different: To generate an instantiation, the compiler needs to have the code that defines a function template or class template member function. As a result, unlike nontemplate code, headers for templates typically include definitions as well as declarations. Definitions of function templates and member functions of class templates are ordinarily put into header files.
When we write a template, the code may not be overtly type specific, but template code usually makes some assumptions about the types that will be used. For example, the code inside our original compare function assumes that the argument type has a < operator. If the arguments passed to compare have a < operation, then the code is fine, but not otherwise. For example:
|
|
This instantiation generates a version of the function that will not compile. However, errors such as this one cannot be detected until the compiler instantiates the definition of compare on type Sales_data.
Class Templates
As an example, we’ll implement a template version of StrBlob. We’ll name our template Blob to indicate that it is no longer specific to strings.
|
|
As we’ve seen many times, when we use a class template, we must supply extra information. We can now see that that extra information is a list of explicit template arguments that are bound to the template’s parameters. For example, to define a type from our Blob template, we must provide the element type:
|
|
The compiler generates a different class for each element type we specify. Each instantiation of a class template constitutes an independent class. The type Blob
|
|
Member Functions of Class Templates
As with any class, we can define the member functions of a class template either inside or outside of the class body. As with any other class, members defined inside the class body are implicitly inline. As usual, when we define a member outside its class, we must say to which class the member belongs. We’ll start by defining the check member, which verifies a given index:
|
|
The subscript operator and back function use the template parameter to specify the return type but are otherwise unchanged:
|
|
In our original StrBlob class these operators returned string&. The template versions will return a reference to whatever type is used to instantiate Blob.
As with any other member defined outside a class template, a constructor starts by declaring the template parameters for the class template of which it is a member:
|
|
Similarly, the constructor that takes an initializer_list uses its type parameter T as the element type for its initializer_list parameter:
|
|
To use this constructor, we must pass an initializer_list in which the elements are compatible with the element type of the Blob:
|
|
By default, a member of an instantiated class template is instantiated only if the member is used. For example, this code:
|
|
There is one exception to the rule that we must supply template arguments when we use a class template type. Inside the scope of the class template itself, we may use the name of the template without arguments:
|
|
Careful readers will have noted that the prefix increment and decrement members of BlobPtr return BlobPtr&, not BlobPtr
|
|
When we define members outside the body of a class template, we must remember that we are not in the scope of the class until the class name is seen:
|
|
Because the return type appears outside the scope of the class, we must specify that the return type returns a BlobPtr instantiated with the same type as the class. Inside the function body, we are in the scope of the class so do not need to repeat the template argument when we define ret. Hence, the definition of ret is as if we had written:
|
|
Inside the scope of a class template, we may refer to the template without specifying template argument(s).
Class Templates and Friends
When a class contains a friend declaration, the class and the friend can independently be templates or not. A class template that has a nontemplate friend grants that friend access to all the instantiations of the template. When the friend is itself a template, the class granting friendship controls whether friendship includes all instantiations of the template or only specific instantiation(s).
In order to refer to a specific instantiation of a template (class or function) we must first declare the template itself.
|
|
A class can also make every instantiation of another template its friend, or it may limit friendship to a specific instantiation:
|
|
To allow all instantiations as friends, the friend declaration must use template parameter(s) that differ from those used by the class itself.
Under the new standard, we can make a template type parameter a friend:
|
|
Here we say that whatever type is used to instantiate Bar is a friend.
Template Type Aliases
we can define a typedef that refers to that instantiated class:
|
|
Because a template is not a type, we cannot define a typedef that refers to a template. However, the new standard lets us define a type alias for a class template:
|
|
A template type alias is a synonym for a family of classes:
|
|
static Members of Class Templates
|
|
Each instantiation of Foo has its own instance of the static members.
|
|
As with any other static data member, there must be exactly one definition of each static data member of a template class. We define a static data member as a template similarly to how we define the member functions of that template:
|
|
When Foo is instantiated for a particular template argument type, a separate ctr will be instantiated for that class type and initialized to 0.
As with static members of nontemplate classes, we can access a static member of a class template through an object of the class type or by using the scope operator to access the member directly. To use a static member through the class, we must refer to a specific instantiation:
|
|
Like any other member function, a static member function is instantiated only if it is used in a program.
Template Parameters
Like the names of function parameters, a template parameter name has no intrinsic meaning. We ordinarily name type parameters T, but we can use any name:
|
|
Because a parameter name cannot be reused, the name of a template parameter can appear only once with in a given template parameter list:
|
|
A template declaration must include the template parameters :
|
|
The names of a template parameter need not be the same across the declaration(s) and the definition of the same template:
|
|
Declarations for all the templates needed by a given file usually should appear together at the beginning of a file before any code that uses those names.
Using Class Members That Are Types
Recall that we use the scope operator (::) to access both static members and type members. In ordinary code, the compiler knows whether a name accessed through the scope operator is a type or a static member. Assuming T is a template type parameter, When the compiler sees code such as T::mem it won’t know until instantiation time whether mem is a type or a static data member. However, in order to process the template, the compiler must know whether a name represents a type. For example:
|
|
it needs to know whether we’re defining a variable named p or are multiplying a static data member named size_type by a variable named p.
By default, the language assumes that a name accessed through the scope operator is not a type. As a result, if we want to use a type member of a template type parameter, we must explicitly tell the compiler that the name is a type. We do so by using the keyword typename:
|
|
When we want to inform the compiler that a name represents a type, we must use the keyword typename, not class.
Default Template Arguments
Under the new standard, we can supply default arguments for both function and class templates. Earlier versions of the language, allowed default arguments only with class templates.
|
|
When users call this version of compare, they may supply their own comparison operation but are not required to do so:
|
|
The first call uses the default function argument, which is a default-initialized object of type less
As with function default arguments, a template parameter may have a default argument only if all of the parameters to its right also have default arguments.
Whenever we use a class template, we must always follow the template’s name with brackets. In particular, if a class template provides default arguments for all of its template parameters, and we want to use those defaults, we must put an empty bracket pair following the template’s name:
|
|
Member Templates
A class—either an ordinary class or a class template—may have a member function that is itself a template. Such members are referred to as member templates. Member templates may not be virtual. As an example of an ordinary class that has a member template, we’ll define a class that is similar to the default deleter type used by unique_ptr. Like the default deleter, our class will have an overloaded function-call operator that will take a pointer and execute delete on the given pointer.
|
|
We can use this class as a replacement for delete:
|
|
Because calling a DebugDelete object deletes its given pointer, we can also use DebugDelete as the deleter of a unique_ptr.
|
|
The unique_ptr destructor calls the DebugDelete’s call operator. Thus, whenever unique_ptr’s destructor is instantiated, DebugDelete’s call operator will also be instantiated: Thus, the definitions above will instantiate:
|
|
We can also define a member template of a class template. In this case, both the class and the member have their own, independent, template parameters. As an example, we’ll give our Blob class a constructor that will take two iterators denoting a range of elements to copy.
|
|
Unlike ordinary function members of class templates, member templates are function templates. When we define a member template outside the body of a class template, we must provide the template parameter list for the class template and for the function template. The parameter list for the class template comes first, followed by the member’s own template parameter list:
|
|
To instantiate a member template of a class template, we must supply arguments for the template parameters for both the class and the function templates.
|
|
Controlling Instantiations
The fact that instantiations are generated when a template is used means that the same instantiation may appear in multiple object files. In large systems, the overhead of instantiating the same template in multiple files can become significant. Under the new standard, we can avoid this overhead through an explicit instantiation. An explicit instantiation has the form
|
|
where declaration is a class or function declaration in which all the template parameters are replaced by the template arguments. For example,
|
|
When the compiler sees an extern template declaration, it will not generate code for that instantiation in that file. Declaring an instantiation as extern is a promise that there will be a nonextern use of that instantiation elsewhere in the program. There may be several extern declarations for a given instantiation but there must be exactly one definition for that instantiation.
Because the compiler automatically instantiates a template when we use it, the extern declaration must appear before any code that uses that instantiation:
|
|
The file Application.o will contain instantiations for Blob
|
|
When the compiler sees an instantiation definition (as opposed to a declaration), it generates code. Thus, the file templateBuild.o will contain the definitions for compare instantiated with int and for the Blob
There must be an explicit instantiation definition somewhere in the program for every instantiation declaration.
An instantiation definition for a class template instantiates all the members of that template including inline member functions. When the compiler sees an instantiation definition it cannot know which member functions the program uses. Hence, unlike the way it handles ordinary class template instantiations, the compiler instantiates all the members of that class. Even if we do not use a member, that member will be instantiated. Consequently, we can use explicit instantiation only for types that can be used with all the members of that template.
Efficiency and Flexibility
The obvious difference between shared_ptr and unique_ptr is the strategy they use in managing the pointer they hold—one class gives us shared ownership; the other owns the pointer that it holds. This difference is essential to what these classes do. These classes also differ in how they let users override their default deleter.
We can easily override the deleter of a shared_ptr by passing a callable object when we create or reset the pointer. In contrast, the type of the deleter is part of the type of a unique_ptr object. Users must supply that type as an explicit template argument when they define a unique_ptr.
Although we don’t know how the library types are implemented, we can infer that shared_ptr must access its deleter indirectly. That is the deleter must be stored as a pointer or as a class that encapsulates a pointer. We can be certain that shared_ptr does not hold the deleter as a direct member, because the type of the deleter isn’t known until run time. Indeed, we can change the type of the deleter in a given shared_ptr during that shared_ptr’s lifetime. We can construct a shared_ptr using a deleter of one type, and subsequently use reset to give that same shared_ptr a different type of deleter.
Now, let’s think about how unique_ptr might work. In this class, the type of the deleter is part of the type of the unique_ptr. That is, unique_ptr has two template parameters, one that represents the pointer that the unique_ptr manages and the other that represents the type of the deleter. Because the type of the deleter is part of the type of a unique_ptr, the type of the deleter member is known at compile time. The deleter can be stored directly in each unique_ptr object.
By binding the deleter at compile time, unique_ptr avoids the run-time cost of an indirect call to its deleter. By binding the deleter at run time, shared_ptr makes it easier for users to override the deleter.
Conversions and Template Type Parameters
The process of determining the template arguments from the function arguments is known as template argument deduction. During template argument deduction, the compiler uses types of the arguments in the call to find the template arguments that generate a version of the function that best matches the given call.
As with a nontemplate function, the arguments we pass in a call to a function template are used to initialize that function’s parameters. Only a very limited number of conversions are automatically applied to such arguments. Rather than converting the arguments, the compiler generates a new instantiation.
As usual, top-level consts in either the parameter or the argument are ignored. The only other conversions performed in a call to a function template are
- const conversions: A function parameter that is a reference (or pointer) to a const can be passed a reference (or pointer) to a nonconst object.
- Array- or function-to-pointer conversions: If the function parameter is not a reference type, then the normal pointer conversion will be applied to arguments of array or function type. An array argument will be converted to a pointer to its first element. Similarly, a function argument will be converted to a pointer to the function’s type.
Other conversions, such as the arithmetic conversions, derived-to-base, and user-defined conversions, are not performed. As examples, consider calls to the functions fobj and fref. The fobj function copies its parameters, whereas fref’s parameters are references:
|
|
In the first pair of calls, we pass a string and a const string. Even though these types do not match exactly, both calls are legal. In the call to fobj, the arguments are copied, so whether the original object is const doesn’t matter. In the call to fref, the parameter type is a reference to const. Conversion to const for a reference parameter is a permitted conversion, so this call is legal.
In the next pair of calls, we pass array arguments in which the arrays are different sizes and hence have different types. In the call to fobj, the fact that the array types differ doesn’t matter. Both arrays are converted to pointers. The template parameter type in fobj is int*. The call to fref, however, is illegal. When the parameter is a reference, the arrays are not converted to pointers. The types of a and b don’t match, so the call is in error.
const conversions and array or function to pointer are the only automatic conversions for arguments to parameters with template types.
Function Parameters That Use the Same Template Parameter Type
A template type parameter can be used as the type of more than one function parameter. Because there are limited conversions, the arguments to such parameters must have essentially the same type. If the deduced types do not match, then the call is an error. For example, our compare function takes two const T& parameters.
|
|
These types don’t match, so template argument deduction fails. If we want to allow normal conversions on the arguments, we can define the function with two type parameters:
|
|
Normal Conversions Apply for Ordinary Arguments
A function template can have parameters that are defined using ordinary types—that is, types that do not involve a template type parameter. Such arguments have no special processing; they are converted as usual to the corresponding type of the parameter.
|
|
Because the type of os is fixed, normal conversions are applied to arguments passed to os when print is called:
|
|
Normal conversions are applied to arguments whose type is not a template parameter.
Function-Template Explicit Arguments
We can let the user control the type of the return by defining a third template parameter to represent the return type:
|
|
In this case, there is no argument whose type can be used to deduce the type of T1. The caller must provide an explicit template argument for this parameter on each call to sum. Explicit template arguments are specified inside angle brackets after the function name and before the argument list:
|
|
Explicit template argument(s) are matched to corresponding template parameter(s) from left to right; the first template argument is matched to the first template parameter, the second argument to the second parameter, and so on. An explicit template argument may be omitted only for the trailing (right-most) parameters, and then only if these can be deduced from the function parameters. If our sum function had been written as
|
|
then we would always have to specify arguments for all three parameters:
|
|
Normal conversions also apply for arguments whose template type parameter is explicitly specified:
|
|
As we’ve seen, the first call is in error because the arguments to compare must have the same type. If we explicitly specify the template parameter type, normal conversions apply.
Trailing Return Type
Using an explicit template argument to represent a template function’s return type works well when we want to let the user determine the return type. In other cases, requiring an explicit template argument imposes a burden on the user with no compensating advantage.
|
|
We don’t know the exact type we want to return, but we do know that we want that type to be a reference to the element type of the sequence we’re processing:
|
|
Here, we know that our function will return *beg, and we know that we can use decltype(*beg) to obtain the type of that expression. However, beg doesn’t exist until the parameter list has been seen. To define this function, we must use a trailing return type. Because a trailing return appears after the parameter list, it can use the function’s parameters:
|
|
Type Transformation
Sometimes we do not have direct access to the type that we need. For example, we might want to write a function similar to fcn that returns an element by value, rather than a reference to an element. The problem we face in writing this function is that we know almost nothing about the types we’re passed. In this function, the only operations we know we can use are iterator operations, and there are no iterator operations that yield elements.
To obtain the element type, we can use a library type transformation template. These templates are defined in the type_traits header. In general the classes in type_traits are used for so-called template metaprogramming, a topic that is beyond the scope of this Primer. However, the type transformation templates are useful in ordinary programming as well.
Standard Type Transformation Templates:
|
|
The remove_reference template has one template type parameter and a (public) type member named type. If we instantiate remove_reference with a reference type, then type will be the referred-to type. For example, if we instantiate remove_reference<int&>, the type member will be int.
|
|
will be the type of the element to which beg refers: decltype(*beg) returns the reference type of the element type. remove_reference::type strips off the reference, leaving the element type itself. Using remove_reference and a trailing return with decltype, we can write our function to return a copy of an element’s value:
|
|
Note that type is member of a class that depends on a template parameter. As a result, we must use typename in the declaration of the return type to tell the compiler that type represents a type.
If it is not possible (or not necessary) to transform the template’s parameter, the type member is the template parameter type itself. For example, if T is a pointer type, then remove_pointer
Function Pointers and Argument Deduction
When we initialize or assign a function pointer from a function template, the compiler uses the type of the pointer to deduce the template argument(s).
|
|
The type of the parameters in pf1 determines the type of the template argument for T. The template argument for T is int. It is an error if the template arguments cannot be determined from the function pointer type:
|
|
We can disambiguate the call to func by using explicit template arguments:
|
|
Template Argument Deduction and References
When a function parameter is an ordinary (lvalue) reference to a template type parameter (i.e., that has the form T&), the binding rules say that we can pass only an lvalue (e.g., a variable or an expression that returns a reference type). That argument might or might not have a const type. If the argument is const, then T will be deduced as a const type:
|
|
If a function parameter has type const T&, normal binding rules say that we can pass any kind of argument—an object (const or otherwise), a temporary, or a literal value. When the function parameter is itself const, the type deduced for T will not be a const type. The const is already part of the function parameter type; therefore, it does not also become part of the template parameter type:
|
|
When a function parameter is an rvalue reference (i.e., has the form T&&), normal binding rules say that we can pass an rvalue to this parameter. When we do so, type deduction behaves similarly to deduction for an ordinary lvalue reference function parameter. The deduced type for T is the type of the rvalue:
|
|
Reference Collapsing and Rvalue Reference Parameters
Assuming i is an int object, we might think that a call such as f3(i) would be illegal. After all, i is an lvalue, and normally we cannot bind an rvalue reference to an lvalue. However, the language defines two exceptions to normal binding rules that allow this kind of usage. These exceptions are the foundation for how library facilities such as move operate.
The first exception affects how type deduction is done for rvalue reference parameters. When we pass an lvalue (e.g., i) to a function parameter that is an rvalue reference to a template type parameter (e.g, T&&), the compiler deduces the template type parameter as the argument’s lvalue reference type. So, when we call f3(i), the compiler deduces the type of T as int&, not int. Deducing T as int& would seem to mean that f3’s function parameter would be an rvalue reference to the type int&. Ordinarily, we cannot (directly) define a reference to a reference. However, it is possible to do so indirectly through a type alias or through a template type parameter.
In such contexts, we see the second exception to the normal binding rules: If we indirectly create a reference to a reference, then those references “collapse.” In all but one case, the references collapse to form an ordinary lvalue reference type. The new standard, expanded the collapsing rules to include rvalue references. References collapse to form an rvalue reference only in the specific case of an rvalue reference to an rvalue reference. That is, for a given type X:
- X& &, X& &&, and X&& & all collapse to type X&
- The type X&& && collapses to X&&
Reference collapsing applies only when a reference to a reference is created indirectly, such as in a type alias or a template parameter.
The combination of the reference collapsing rule and the special rule for type deduction for rvalue reference parameters means that we can call f3 on an lvalue. When we pass an lvalue to f3’s (rvalue reference) function parameter, the compiler will deduce T as an lvalue reference type:
|
|
When a template parameter T is deduced as a reference type, the collapsing rule says that the function parameter T&& collapses to an lvalue reference type. For example, the resulting instantiation for f3(i) would be something like
|
|
The function parameter in f3 is T&& and T is int&, so T&& is int& &&, which collapses to int&. Thus, even though the form of the function parameter in f3 is an rvalue reference (i.e., T&&), this call instantiates f3 with an lvalue reference type (i.e., int&):
|
|
There are two important consequences from these rules:
- A function parameter that is an rvalue reference to a template type parameter (e.g., T&&) can be bound to an lvalue; and
- If the argument is an lvalue, then the deduced template argument type will be an lvalue reference type and the function parameter will be instantiated as an (ordinary) lvalue reference parameter (T&)
It is also worth noting that by implication, we can pass any type of argument to a T&& function parameter. A parameter of such a type can (obviously) be used with rvalues, and as we’ve just seen, can be used by lvalues as well. An argument of any type can be passed to a function parameter that is an rvalue reference to a template parameter type (i.e., T&&). When an lvalue is passed to such a parameter, the function parameter is instantiated as an ordinary, lvalue reference (T&).
The fact that the template parameter can be deduced to a reference type can have surprising impacts on the code inside the template:
|
|
In practice, rvalue reference parameters are used in one of two contexts: Either the template is forwarding its arguments, or the template is overloaded. For now, it’s worth noting that function templates that use rvalue references often use overloading in the same way as we saw in:
|
|
Understanding std::move
Although we cannot directly bind an rvalue reference to an lvalue, we can use move to obtain an rvalue reference bound to an lvalue. Because move can take arguments of essentially any type, it should not be surprising that move is a function template. The standard defines move as follows:
|
|
This code is short but subtle. First, move’s function parameter, T&&, is an rvalue reference to a template parameter type. Through reference collapsing, this parameter can match arguments of any type. In particular, we can pass either an lvalue or an rvalue to move:
|
|
In the first assignment, the argument to move is the rvalue result of the string constructor, string(“bye”). As we’ve seen, when we pass an rvalue to an rvalue reference function parameter, the type deduced from that argument is the referred-to type. Thus, in std::move(string(“bye!”)):
- The deduced type of T is string.
- Therefore, remove_reference is instantiated with string.
- The type member of remove_reference
is string. - The return type of move is string&&.
- move’s function parameter, t, has type string&&.
Accordingly, this call instantiates move
|
|
The body of this function returns static_cast<string&&>(t). The type of t is already string&&, so the cast does nothing. Therefore, the result of this call is the rvalue reference it was given.
Now consider the second assignment, which calls std::move(s1). In this call, the argument to move is an lvalue. This time:
- The deduced type of T is string& (reference to string, not plain string).
- Therefore, remove_reference is instantiated with string&.
- The type member of remove_reference<string&> is string.
- The return type of move is still string&&.
- move’s function parameter, t, instantiates as string& &&, which collapses to string&.
Thus, this call instantiates move<string&>, which is
|
|
The body of this instantiation returns static_cast<string&&>(t). In this case, the type of t is string&, which the cast converts to string&&.
Ordinarily, a static_cast can perform only otherwise legitimate conversions. However, there is again a special dispensation for rvalue references: Even though we cannot implicitly convert an lvalue to an rvalue reference, we can explicitly cast an lvalue to an rvalue reference using static_cast.
clobber: (Programming) To make a change or a call in a program which unintentionally overwrites the current value of a variable. RAII: Resource Acquisition Is Initialization, use destructor to free memory when there is an exception, so C++ do not need GC.
Binding an rvalue reference to an lvalue gives code that operates on the rvalue reference permission to clobber the lvalue. There are times, such as in our StrVec reallocate function, when we know it is safe to clobber an lvalue. By letting us do the cast, the language allows this usage. By forcing us to use a cast, the language tries to prevent us from doing so accidentally. Finally, although we can write such casts directly, it is much easier to use the library move function. Moreover, using std::move consistently makes it easy to find the places in our code that might potentially clobber lvalues.
Forwarding
Some functions need to forward one or more of their arguments with their types unchanged to another, forwarded-to, function. In such cases, we need to preserve everything about the forwarded arguments, including whether or not the argument type is const, and whether the argument is an lvalue or an rvalue. As an example, we’ll write a function that takes a callable expression and two additional arguments.
|
|
The problem is that j is passed to the t1 parameter in flip1. That parameter has is a plain, nonreference type, int, not an int&.
To pass a reference through our flip function, we need to rewrite our function so that its parameters preserve the “lvalueness” of its given arguments. Thinking ahead a bit, we can imagine that we’d also like to preserve the constness of the arguments as well. We can preserve all the type information in an argument by defining its corresponding function parameter as an rvalue reference to a template type parameter. Using a reference parameter (either lvalue or rvalue) lets us preserve constness, because the const in a reference type is low-level. Through reference collapsing, if we define the function parameters as T1&& and T2&&, we can preserve the lvalue/rvalue property of flip’s arguments:
|
|
This version of flip2 solves one half of our problem. Our flip2 function works fine for functions that take lvalue references but cannot be used to call a function that has an rvalue reference parameter. For example:
|
|
what is passed to g will be the parameter named t2 inside flip2. A function parameter, like any other variable, is an lvalue expression. As a result, the call to g in flip2 passes an lvalue to g’s rvalue reference parameter.
We can use a new library facility named forward to pass flip2’s parameters in a way that preserves the types of the original arguments. Like move, forward is defined in the utility header. Unlike move, forward must be called with an explicit template argument. forward returns an rvalue reference to that explicit argument type. That is, the return type of forward
Ordinarily, we use forward to pass a function parameter that is defined as an rvalue reference to a template type parameter. Through reference collapsing on its return type, forward preserves the lvalue/rvalue nature of its given argument:
|
|
If that argument was an rvalue, then Type is an ordinary (nonreference) type and forward
|
|
If we call flip(g, i, 42), i will be passed to g as an int& and 42 will be passed as an int&&.
Overloading and Templates
Function templates can be overloaded by other templates or by ordinary, nontemplate functions. As usual, functions with the same name must differ either as to the number or the type(s) of their parameters.
-
The candidate functions for a call include any function-template instantiation for which template argument deduction succeeds.
-
The candidate function templates are always viable, because template argument deduction will have eliminated any templates that are not viable.
-
As usual, the viable functions (template and nontemplate) are ranked by the conversions, if any, needed to make the call. Of course, the conversions used to call a function template are quite limited.
-
Also as usual, if exactly one function provides a better match than any of the others, that function is selected. However, if there are several functions that provide an equally good match, then:
-
If there is only one nontemplate function in the set of equally good matches, the nontemplate function is called.
-
If there are no nontemplate functions in the set, but there are multiple function templates, and one of these templates is more specialized than any of the others, the more specialized function template is called.
-
Otherwise, the call is ambiguous.
Writing Overloaded Templates
As an example, we’ll build a set of functions that might be useful during debugging.
|
|
This function can be used to generate a string corresponding to an object of any type that has an output operator.
|
|
This version generates a string that contains the pointer’s own value and calls debug_rep to print the object to which that pointer points. Note that this function can’t be used to print character pointers, because the IO library defines a version of the « for char* values. That version of « assumes the pointer denotes a null-terminated character array, and prints the contents of the array, not its address.
|
|
For this call, only the first version of debug_rep is viable. The second version of debug_rep requires a pointer parameter, and in this call we passed a nonpointer object. If we call debug_rep with a pointer:
|
|
both functions generate viable instantiations:
- debug_rep(const string* &), which is the instantiation of the first version of debug_rep with T bound to string*
- debug_rep(string*), which is the instantiation of the second version of debug_rep with T bound to string
The instantiation of the second version of debug_rep is an exact match for this call. The instantiation of the first version requires a conversion of the plain pointer to a pointer to const. Normal function matching says we should prefer the second template, and indeed that is the one that is run.
Multiple Viable Templates
As another example, consider the following call:
|
|
Here both templates are viable and both provide an exact match:
- debug_rep(const string* &), the instantiation of the first version of the template with T bound to const string*
- debug_rep(const string*), the instantiation of the second version of the template with T bound to const string
In this case, normal function matching can’t distinguish between these two calls. We might expect this call to be ambiguous. However, due to the special rule for overloaded function templates, this call resolves to debug_rep(T*), which is the more specialized template.
The reason for this rule is that without it, there would be no way to call the pointer version of debug_rep on a pointer to const. The problem is that the template debug_rep(const T&) can be called on essentially any type, including pointer types. That template is more general than debug_rep(T*), which can be called only on pointer types. Without this rule, calls that passed pointers to const would always be ambiguous. When there are several overloaded templates that provide an equally good match for a call, the most specialized version is preferred.
Nontemplate and Template Overloads
For our next example, we’ll define an ordinary nontemplate version of debug_rep to print strings inside double quotes:
|
|
Now, when we call debug_rep on a string,
|
|
there are two equally good viable functions:
- debug_rep
(const string&), the first template with T bound to string - debug_rep(const string&), the ordinary, nontemplate function
In this case, both functions have the same parameter list, so obviously, each function provides an equally good match for this call. However, the nontemplate version is selected. For the same reasons that the most specialized of equally good function templates is preferred, a nontemplate function is preferred over equally good match(es) to a function template.
Overloaded Templates and Conversions
There’s one case we haven’t covered so far: pointers to C-style character strings and string literals. Now that we have a version of debug_rep that takes a string, we might expect that a call that passes character strings would match that version. However, consider this call:
|
|
Here all three of the debug_rep functions are viable:
- debug_rep(const T&), with T bound to char[10]
- debug_rep(T*), with T bound to const char
- debug_rep(const string&), which requires a conversion from const char* to string
Both templates provide an exact match to the argument—the second template requires a (permissible) conversion from array to pointer, and that conversion is considered as an exact match for function-matching purposes. As before, the T* version is more specialized and is the one that will be selected.
If we want to handle character pointers as strings, we can define two more nontemplate overloads:
|
|
Missing Declarations Can Cause the Program to Misbehave
It is worth noting that for the char* versions of debug_rep to work correctly, a declaration for debug_rep(const string&) must be in scope when these functions are defined. If not, the wrong version of debug_rep will be called:
|
|
Ordinarily, if we use a function that we forgot to declare, our code won’t compile. Not so with functions that overload a template function. If the compiler can instantiate the call from the template, then the missing declaration won’t matter. In this example, if we forget to declare the version of debug_rep that takes a string, the compiler will silently instantiate the template version that takes a const T&.
Declare every function in an overload set before you define any of the functions. That way you don’t have to worry whether the compiler will instantiate a call before it sees the function you intended to call.
Variadic Templates
A variadic template is a template function or class that can take a varying number of parameters. The varying parameters are known as a parameter pack. There are two kinds of parameter packs: A template parameter pack represents zero or more template parameters, and a function parameter pack represents zero or more function parameters.
We use an ellipsis to indicate that a template or function parameter represents a pack. In a template parameter list, class… or typename… indicates that the following parameter represents a list of zero or more types; the name of a type followed by an ellipsis represents a list of zero or more nontype parameters of the given type. In the function parameter list, a parameter whose type is a template parameter pack is a function parameter pack. For example:
|
|
declares that foo is a variadic function that has one type parameter named T and a template parameter pack named Args. As usual, the compiler deduces the template parameter types from the function’s arguments.
|
|
the compiler will instantiate four different instances of foo:
|
|
When we need to know how many elements there are in a pack, we can use the sizeof… operator. Like sizeof, sizeof… returns a constant expression and does not evaluate its argument:
|
|
We can use an initializer_list to define a function that can take a varying number of arguments. However, the arguments must have the same type (or types that are convertible to a common type). Variadic functions are used when we know neither the number nor the types of the arguments we want to process. As an example, we’ll define a function like our earlier error_msg function, only this time we’ll allow the argument types to vary as well.
Variadic functions are often recursive. The first call processes the first argument in the pack and calls itself on the remaining arguments. Our print function will execute this way—each call will print its second argument on the stream denoted by its first argument.
|
|
For the last call of print in the recursion, the variadic version is also viable. Unlike an ordinary argument, a parameter pack can be empty. However, a nonvariadic template is more specialized than a variadic template, so the nonvariadic version is chosen for this call.
Pack Expansion
When we expand a pack, we also provide a pattern to be used on each expanded element. Expanding a pack separates the pack into its constituent elements, applying the pattern to each element as it does so. We trigger an expansion by putting an ellipsis (. . . ) to the right of the pattern.
|
|
The first expansion expands the template parameter pack and generates the function parameter list for print. The second expansion appears in the call to print. That pattern generates the argument list for the call to print.
The expansion of the function parameter pack in print just expanded the pack into its constituent parts. More complicated patterns are also possible when we expand a function parameter pack. For example, we might write a second variadic function that calls debug_rep on each of its arguments and then calls print to print the resulting strings:
|
|
The call to print uses the pattern debug_rep(rest). That pattern says that we want to call debug_rep on each element in the function parameter pack rest. The resulting expanded pack will be a comma-separated list of calls to debug_rep. That is, a call such as
|
|
will execute as if we had written
|
|
In contrast, the following pattern would fail to compile:
|
|
Forwarding Parameter Packs
Under the new standard, we can use variadic templates together with forward to write functions that pass their arguments unchanged to some other function. To illustrate such functions, we’ll add an emplace_back member to our StrVec class. The emplace_back member of the library containers is a variadic member template that uses its arguments to construct an element directly in space managed by the container.
Our version of emplace_back for StrVec will also have to be variadic, because string has a number of constructors that differ in terms of their parameters. Because we’d like to be able to use the string move constructor, we’ll also need to preserve all the type information about the arguments passed to emplace_back.
As we’ve seen, preserving type information is a two-step process. First, to preserve type information in the arguments, we must define emplace_back’s function parameters as rvalue references to a template type parameter:
|
|
The pattern in the expansion of the template parameter pack, &&, means that each function parameter will be an rvalue reference to its corresponding argument.
Second, we must use forward to preserve the arguments’ original types when emplace_back passes those arguments to construct:
|
|
By using forward in this call, we guarantee that if emplace_back is called with an rvalue, then construct will also get an rvalue. For example, in this call:
|
|
the argument to emplace_back is an rvalue, which is passed to construct as
|
|
The result type from forward
Template Specializations
In some cases, the general template definition is simply wrong for a type: The general definition might not compile or might do the wrong thing. When we can’t (or don’t want to) use the template version, we can define a specialized version of the class or function template.
Our compare function is a good example of a function template for which the general definition is not appropriate for a particular type, namely, character pointers. We’d like compare to compare character pointers by calling strcmp rather than by comparing the pointer values. Indeed, we have already overloaded the compare function to handle character string literals:
|
|
However, the version of compare that has two nontype template parameters will be called only when we pass a string literal or an array. If we call compare with character pointers, the first version of the template will be called:
|
|
There is no way to convert a pointer to a reference to an array, so the second version of compare is not viable when we pass p1 and p2 as arguments. To handle character pointers (as opposed to arrays), we can define a template specialization of the first version of compare. A specialization is a separate definition of the template in which one or more template parameters are specified to have particular types.
When we specialize a function template, we must supply arguments for every template parameter in the original template. To indicate that we are specializing a template, we use the keyword template followed by an empty pair of angle brackets (< >). The empty brackets indicate that arguments will be supplied for all the template parameters of the original template:
|
|
When we define a specialization, the function parameter type(s) must match the corresponding types in a previously declared template. Here we are specializing:
|
|
in which the function parameters are references to a const type. As with type aliases, the interaction between template parameter types, pointers, and const can be surprising. We want to define a specialization of this function with T as const char*. Our function requires a reference to the const version of this type. The const version of a pointer type is a constant pointer as distinct from a pointer to const. The type we need to use in our specialization is const char* const &, which is a reference to a const pointer to const char.
Function Overloading versus Template Specializations
When we define a function template specialization, we are essentially taking over the job of the compiler. That is, we are supplying the definition to use for a specific instantiation of the original template. It is important to realize that a specialization is an instantiation; it is not an overloaded instance of the function name. Specializations instantiate a template; they do not overload it. As a result, specializations do not affect function matching. Whether we define a particular function as a specialization or as an independent, nontemplate function can impact function matching. For example, we have defined two versions of our compare function template, one that takes references to array parameters and the other that takes const T&. The fact that we also have a specialization for character pointers has no impact on function matching.
In order to specialize a template, a declaration for the original template must be in scope. Moreover, a declaration for a specialization must be in scope before any code uses that instantiation of the template. With ordinary classes and functions, missing declarations are (usually) easy to find—the compiler won’t be able to process our code. However, if a specialization declaration is missing, the compiler will usually generate code using the original template.
It is an error for a program to use a specialization and an instantiation of the original template with the same set of template arguments. However, it is an error that the compiler is unlikely to detect.
Templates and their specializations should be declared in the same header file. Declarations for all the templates with a given name should appear first, followed by any specializations of those templates.
Class Template Specializations
In addition to specializing function templates, we can also specialize class templates. As an example, we’ll define a specialization of the library hash template that we can use to store Sales_data objects in an unordered container. By default, the unordered containers use hash<key_type> to organize their elements. To use this default with our own data type, we must define a specialization of the hash template. A specialized hash class must define
- An overloaded call operator that returns a size_t and takes an object of the container’s key type
- Two type members, result_type and argument_type, which are the return and argument types, respectively, of the call operator
- The default constructor and a copy-assignment operator (which can be implicitly defined)
The only complication in defining this hash specialization is that when we specialize a template, we must do so in the same namespace in which the original template is defined. For now, what we need to know is that we can add members to a namespace. To do so, we must first open the namespace:
|
|
Any definitions that appear between the open and close curlies will be part of the std namespace. The following defines a specialization of hash for Sales_data:
|
|
Assuming our specialization is in scope, it will be used automatically when we use Sales_data as a key to one of these containers:
|
|
Because hash<Sales_data> uses the private members of Sales_data, we must make this class a friend of Sales_data:
|
|
To enable users of Sales_data to use the specialization of hash, we should define this specialization in the Sales_data header.
Class-Template Partial Specializations
Differently from function templates, a class template specialization does not have to supply an argument for every template parameter. We can specify some, but not all, of the template parameters or some, but not all, aspects of the parameters. A class template partial specialization is itself a template. Users must supply arguments for those template parameters that are not fixed by the specialization. We can partially specialize only a class template. We cannot partially specialize a function template.
|
|
The template parameter list of a partial specialization is a subset of, or a specialization of, the parameter list of the original template.
|
|
All three variables, a, b, and c, have type int.
Specializing Members but Not the Class
Rather than specializing the whole template, we can specialize just specific member function(s). For example, if Foo is a template class with a member Bar, we can specialize just that member:
|
|
Here we are specializing just one member of the Foo
|
|
文章作者 huijian142857
上次更新 2015-10-08