1. Conversion operators
Sometimes, an object must be converted into a built-in type (for instance, a string object passed as an argument to C function such as
strcmp())
:
//file Mystring.h
class Mystring {
char *s;
int size;
public:
Mystring(const char *);
Mystring();
//...
};
#include //C str- family of functions
#include "Mystring.h"
void main() {
Mystring str("hello world");
int n = strcmp(str, "Hello"); //compile time error: str is not of //type const char *
}//end main()
C++ offers an automatic type conversion for such cases. All you have to do is declare a conversion operator in your class definition:
class Mystring { //now with conversion operator
char *s;
int size;
public:
Mystring(const char *);
Mystring();
operator const char * () { return s; } //conversion operator
//...
};
And all is fine:
int n = strcmp(str, "Hello"); //now OK, automatic conversion to //const char *
Important: conversion operator is different than an ordinary overloaded operator: it should not return a value (not even void) and takes no arguments.
2. Uses of the 'volatile' Keyword
The volatile specifier disables various optimizations that a compiler might automatically apply and thus introduce bugs. I'll give two examples:
1) Some hardware interfacing applications periodically read values from a hardware port and copy it to a local variable for further processing. A good example is a timer function that reads the current count from an external clock. If the variable is const, the compiler might skip subsequent reads to make the code more efficient and store the result in a CPU register. To force it to read from the port every time the code accesses it, declare the variable const volatile.
2) In multithreaded applications, thread A might change a shared variable behind thread B's back. Because the compiler doesn't see any code that changes the value in thread B, it could assume that B's value has remained unchanged and store it in a CPU register instead of reading it from the main memory. Alas, if thread A changes the variable's value between two invocations of thread B, the latter will have have an incorrect value. To force the compiler not to store a variable in a register, declare it volatile.
3. Overloaded Operators May Not Have Default Parameters
Unlike ordinary functions, overloaded operators cannot declare a parameter with a default value (overloaded operator() is the only exception):
class Date
{
private:
int day, month, year;
public:
Date & operator += (const Date & d = Date() ); //error, default arguments are not allowed
};
This rule may seem arbitrary. However, it captures the behavior of built-in operators, which never have default operands either.
4. Member and Non-Member Overloaded Operators
Most of the overloaded operators can be declared either as non-static class members or as non-member functions. In this example, operator == is overloaded as a non-static class member:
class Date{
//…
public:
bool operator == (const Date & d ); // 1: member function
};
Alternatively, one can declare it as a friend function:
bool operator ==( const Date & d1, const Date& d2); // 2: extern function
class Date{
friend bool operator ==( const Date & d1, const Date& d2);
};
Nonetheless, the operators [], (), = and -> can only be declared as non-static member functions. This ensures that their first operand is a l-value.
5. Overload New and Delete in a Class
It is possible to override the global operators new and delete for a given class. For example, you can use this technique to override the default behavior of operator new in case of a failure. Instead of throwing a std::bad_alloc exception, the class-specific version of new throws a char array:
#include <cstdlib> //declarations of malloc and free
#include <new>
#include <iostream>
using namespace std;
class C {
public:
C();
void* operator new (size_t size); //implicitly declared as a static member function
void operator delete (void *p); //implicitly declared as a static member function
};
void* C::operator new (size_t size) throw (const char *){
void * p = malloc(size);
if (p == 0) throw "allocation failure"; //instead of std::bad_alloc
return p;
}
void C::operator delete (void *p){
C* pc = static_cast<C*>(p);
free(pc);
}
int main() {
C *p = new C; // calls C::new
delete p; // calls C::delete
}
Note that the overloaded new and delete implicitly invoke the object's constructor and destructor, respectively. Remember also to define a matching operator delete when you override operator new.
6. Restrictions on Operator Overloading
The following restrictions apply to operator overloading:
1. Invention of new operators is not allowed. For example:
void operator @ (int) ; //illegal, @ is not a built-in operator or a type name
2. Neither the precedence nor the number of arguments of an operator may be altered. An overloaded && for example, must have exactly two arguments--just like the built-in && operator.
3. The following operators cannot be overloaded:
Direct member access operator .
De-reference pointer to class member operator .*.
Scope resolution operator ::
Conditional operator ?:
Sizeof operator sizeof
Typeid operator typeid
Similarly, any of the new casting operators: static_cast<>, dynamic_cast<>, reinterpret_cast<> and const_cast<>, as well as the # and ## preprocessor tokens, may not be overloaded.
7. Overloading the Subscript Operator the Right Way
It is customary to overload the subscript operator, [], in classes that hold a sequence of elements. Vector and String are examples of such classes. When you overload operator [], remember to define two versions thereof: a non-const version and a const one. For example:
class MyString
{
private:
char * buff;
int size;
public:
//...
char& operator [] (int index) { return buff[index]; } //non-const
const char& operator [] (int index) const { return buff[index]; } //const
};
The const version of the subscript operator is called when its object itself is const:
void f(const MyString& str)
{
char c = str[0]; //calls const char& operator [] (int index) const
}
8. Assignment operator is not inherited
Unlike ordinary base class member functions, assignment operator is not inherited. It may be re-defined by the implementer of the derived class or else the compiler automatically synthesizes it, so there's not much point in declaring it virtual.
9. User-Defined New and Delete Cannot be Declared in a Namespace
Operators new and delete can be declared in a class scope. However, the Standard prohibits declarations of these operators in a namespace. Why is this? Consider the following example:
char *pc;
namespace A {
void operator new ( size_t );
void operator delete ( void * );
void func () {
pc = new char ( 'a');
}
}
void f() { delete pc; } // which version of delete to call, A::delete or standard delete?
Some programmers would expect the operator A::delete to be selected since it matches the operator new that was used to allocate the storage. Others would expect the standard operator delete to be called since A::delete is not visible in function f. By prohibiting declarations of new and delete in a namespace, you can avoid this hassle.
10. Operators Can Only be Overloaded for User-Defined Types
An overloaded operator must take at least one argument of a user-defined type (operators new and delete are an exception). This rule ensures that users cannot alter the meaning of expressions that contain only fundamental types. For example:
int i,j,k;
k = i + j; //always uses built-in = and +
Recall that enum types are user-defined types, and as such, you can define overloaded operators for them too.
11. Simulating Inheritance of Assignment Operator
As opposed to base class' constructor and destructor, which are automatically invoked from the derived class' constructor and destructor respectively, a user-defined assignment operator defined in a base class is overridden - rather than being extended - when re-defined in a derived class. In order to extend the assignment operator in a derived class, one has first to invoke the base's assignment operator explicitly, and then add the assignment operations required for the derived class.
class C {
char *p;
public:
enum {size = 10}; //size serves as a constant
const char * Getp() const {return p;}
C() : p ( new char [size] ) {}
C& operator = (const C& other) {
if (this != &other)
strcpy(p, other.Getp() );
return *this;}
//...destructor and copy constructor
};
class D : public C {
char *q;
public:
const char * Getq() const {return q;}
D(): q ( new char [size] ) {}
D& operator = (const D& other)
{
if (this != &other)
C::operator=(other); //first invoke base's assignment operator explicitly
strcpy(q, (other.Getq())); //add extensions here
return *this;
}
//...destructor and copy constructor
};
12. Prefix Versus Postfix Operators
You can use both -- and ++ as prefix and postfix operators. When applied to primitives such as int or char, they are indistinguishable in terms of efficiency. When applied to objects, on the other hand, the overloaded prefix operators are significantly more efficient than the postfix ones. Therefore, whenever you're free to choose between postfix or prefix operators of an object, you should prefer the latter:
Void f() {
Date d1, d2;
d1++; d2-- //inefficient: postfix operator
++d1; --d2; //prefix is more efficient
}
The reason is that the postfix version usually involves a construction of an extra copy of the object, in which its state is preserved, whereas the prefix version is applied directly to its object:
class Date
{
Date operator++(int unused) { // Date++ is less efficient
Date temp(*this); //create a copy of the current object
this.AddDays(1); //increment current object
return temp; //return by value a copy of the object before it was incremented
}
Date& operator++() { // ++Date is more efficient
this.AddDays(1); //increment current object
return *this; //return by reference the current object
}
};
No comments:
Post a Comment