Let us try to do a case study. Consider the multiplication operator(*).
* Member function Vs File Scope function Consider a class wrapper around the built-in type float. At the minimal you would like to provide arithematic operators for such a class.
Now consider the multiplication operator for the class. The first question. Should that operator be a member or a file-scope function ? Let begin by being object-oriented.
FloatWrapper result = a * b // a.operator*(b) called FloatWrapper result1 = b * a // b.operator*(a) called FloatWrapper result2 = a * 2.0 // a.operator*(FloatWrapper(2.0)). (implicit //conversion constructor specified in the class called by the compiler. FloatWrapper result3 = 2.0 * a // duh!!!!! cannot call (2.0).operator*( a)
The operator* is supposed to be commutative and thus this is NOT the way to go. The option is to go for a file-scope function:
Now this will sweetly preserve the commutative quality of the operator*. Other example of operators which may not be used as filescope are operator<< and operator>>
* Return by value or by reference We have all heard the when we return/pass by reference, there is gain in efficieny since, no constructors/destructors are called to create the temporary object. Let us begin by defining the operator* to return a const reference
...
const FloatWrapper& operator*(const FloatWrapper& lhs, const FloatWrapper& rhs) { FloatWrapper result(lhs.value * rhs.value) //Assuming the operator is a friend return result; }
What is the problem here !) We are returning a reference to a locally defined stack variable which will be destroyed upon exiting the function stack !) Clearly we are returning by reference to avoid making a copy. However, result will have to be constructed just like any other temporary object.
Let us try to construct the object on a heap
...
const FloatWrapper& operator*(const FloatWrapper& lhs, const FloatWrapper& rhs) { FloatWrapper * result = new FloatWrapper(lhs.value * rhs.value) //Assuming the //operator is a //friend return *result; }
Well, !) We still have to pay for the constructor call !) Besides, who will take the ownership of calling delete on this newly created object ? The client ?
The way around this is to make the operator return by value. A good guideline to decide would if you want to return by value or refernce, is identify if you are return an object newly created within the function. Mostly like you want to return by value in such a case.
In the code sample below N is a non type template parameter.
template<class T, size_t N> class Stack {
T data[N]; // Fixed capacity is N
size_t count;
public:
void push(const T& t);
// Etc.
};
* The non-type template parameter MUST be an integral value that is known at compile
time
Default template arguments
In the following code sample the second template parameter has a default argument.
template<class T, size_t N = 100> class Stack {
T data[N]; // Fixed capacity is N
size_t count;
public:
void push(const T& t);
// Etc.
};
* You can provide default arguments for template parameters in class templates, but NOT in function templates. (why?)
* They should only be defined once, the first time a template declaration or definition is seen by the compiler.
* Once you introduce a default argument, all the subsequent template parameters must also have defaults.
* You can choose to provide defaults for all arguments, but you must use an empty set of brackets when declaring an instance so that the compiler knows that a class template is involved.
template<class T = int, size_t N = 100> // Both defaulted
class Stack {
T data[N]; // Fixed capacity is N
size_t count;
public:
void push(const T& t);
// Etc.
};
* If default arguments happen to be templatized as below, put the space between the last two right angle bracket characters as it prevents the compiler from interpreting those two characters (>>) as the right-shift operator.
template<class T, class Allocator = allocator<T> >
class vector;
Template template arguments
Following code sample is an example of a class template with a template parameter as the argument.
* The class Seq is another class template. For example an Array class template could be a trivial sequence container.
* When the compiler looks at the inner parameters of a template template parameter, default arguments are not consideed so you MUST repeat the defaults in order to get a exact match.
The default dimension of 10 is required in the line:
template<class T, template<class, size_t = 10> class Seq>
* The above situation is the ONLY exception to the rule that default arguments must not be repeated in a compilation unit.
The "typename" keyword
* If a type referred to inside template code is qualified by a template type parameter, you must use the typename keyword as a prefix, unless it appears in a base class specification or initializer list in the same scope (in which case you MUST not).
* Without "typename", the compiler would assume that the expression Seq::iterator is not a type in the following code segment. It will mistake it for a static data member.
template<class T, template<class U, class = allocator<U> >
class Seq>
void printSeq(Seq<T>& seq) {
for(typename Seq<T>::iterator b = seq.begin();
b != seq.end();)
cout << *b++ << endl;
}
* "typename can be used as an option to the keyword "class" within the template argument list of a template definition.
Member Function Templates
The complex template in C++ Standard Library has a type parameter meant to represent an underlying floating-point type to hold the real and imaginary parts of a complex number. The following code snippet from the standard library shows a member-template constructor in the complex class template:
Here the member templates enables flexible conversion easy. The complex template parameter T is double whereas X is a float.
* Defining a template within a template is a nesting operation, so the prefixes that introduce the templates must reflect this nesting if you define the member template outside the outer class definition
template<typename T>
template<typename X>
complex<T>::complex(const complex<X>& c) {/* Body here… */}
With this a vector of ints can be used to initialize a vector of doubles. which is legitimate conversion.
* Member templates can also be classes. (They don’t need to be functions.)
* Member template functions cannot be declared virtual. Current compiler technology expects to be able to determine the size of a class’s virtual function table when the class is parsed.
Function Templates Caveats
*While instantiating function templates you can often omit the template arguments. One MUST always use angle brackets when instantiating class.
* Function templates can deduce the type to be used in templates from the types of the function arguments.
* Default template arguments are not even allowed with function templates as opposed to class templates. (why?)
* If template arguments are avoided when instantiating function templates NO standard conversion are performed. For eg. Consider the following function template
template<typename T, typename U>
const T& min(const T& a, const U& b) {
return (a < b) ? a : b;
}
Will fail (As compiler does not know which type to deduce. Double or int?):
int z = min(x, j); // x is a double ; j is int
Will pass:
int z = min<double>(x, j);
Will pass:
int z = min(x, (double)j);
* If the RETURN type is an INDEPENDANT template parameter the function MUST be called with explicit type specification. Consider the following example where R is a template parameter return type.
int main() {
int i = 1;
float x = implicit_cast<float>(i);
int j = implicit_cast<int>(x);
//! char* p = implicit_cast<char*>(i); // Fails because there is no conversion from int to char*
}
If you interchange R and P in the template parameter list near the top of the file, it will be impossible to compile this program because the return type will remain unspecified