Saturday, May 31, 2008

C++ #12

13. Overloading the Function Call Operator
Overloading the function call operator can be somewhat confusing because the overloaded operator has two pairs of parentheses. It may not be immediately obvious which of these pairs declares the parameters. Another point to note is that the overloaded function call operator may take any number of arguments whereas all other overloaded operators take a fixed number of arguments. This example shows how you can overload the () operator and use it:

class A
{
private:
int n;
public:
//…
void operator ()(bool debug) const; //parameters are always
declared in the second pair of parentheses
};

void A::operator ()(bool debug) const //definition
{
if (debug)
cout<< n;
else
cout<<"nodebug";
}
int main()
{
A a;
a(false); //use the overloaded operator
a(true);
}
14. vector<> members must define < and == operators
The STL vector<>, as well as other standard containers, support comparison and sorting of their members by calling the corresponding standard functions found in the <algorithm> header file. These functions rely on the relational operators such as == and < in order to accomplish that. In case of primitive types such as int, char and pointers, the built-in operators are already defined and will be used. User-defined types, however, require overloading of these operators in order to perform a meaningful comparison and sort:

//file: Date.h
class Date {
public:
bool operator == (const Date& d) const
{ return d.Year()==year && d.Month()==month && d.Day()==day;}

bool operator < (const Date& Date) const;
};

//file: myprog.cpp

#include
#include
#include

using namespace std;

void main()
{
Date d1, d2(1970);
vector vd(10);
vd.push_back(d1);
vd.push_back(d2);

//sort uses overloaded == and > of class Date
sort(vd.begin(), vd.end());
}
15. Beware of Aliasing
Whenever your class contains pointers, references, or handles, you need to define a copy constructor and assignment operator. Otherwise, the compiler-generated copy constructor and assignment operator will result in aliasing, that is to say, the same resource will be used simultaneously by more than one object and will also be released more than once - with disastrous results:

class Document {
private:
FILE *pdb;
public:
Document(FILE *f =NULL) : pdb(f){} //no user-defined copy constructor or operator=
~Document() {fclose(pdb);}
//...
};
void assign(Documnet d&)
{
Document temp("letter.doc");
d = temp; //Aliasing; both d and temp now point to the same file
}//temp's destructor is now automatically called and closes file letter.doc while d is still using it
void main()
{
Document doc;
assign(doc);
//OOPS! doc now uses a file which has just been closed
}//OOPS! doc's destructor is now invoked and closes 'letter.doc' once again
16. Deep Copy and Shallow Copy
The terms "deep copy" and "shallow copy" refer to the way objects are copied, for example, during the invocation of a copy constructor or assignment operator. In a deep copy (also called "memberwise copy"), the copy operation respects object semantics. For example, copying an object that has a member of type std::string ensures that the corresponding std::string in the target object is copy-constructed by the copy constructor of class std::string.

class A
{
string s;
};
A a;
A b;
a=b; //deep copy
When assigning b to a, the compiler-generated assignment operator of class A first invokes the assignment operator of class std::string. Thus, a.s and b.s are well-defined, and they are probably not binary-identical. On the other hand, a shallow copy (also called "bitwise copy") simply copies chunks of memory from one location to another. A memcpy() operation is an example of a shallow copy. Because memcpy() does not respect object semantics, it will not invoke the copy constructor of an object. Therefore, you should never use memcpy() to copy objects. Use it only when copying POD (Plain Old Data) types: ints, floating point numbers, and dumb structs.

17. Avoid Using malloc() and free() in C++
The use of malloc() and free() functions in a C++ file is not recommended and is even dangerous:
1. malloc() requires the exact number of bytes as an argument whereas new calculates the size of the allocated object automatically. By using new, silly mistakes such as the following are avoided:
long * p = malloc(sizeof(short)); //p originally pointed to a short;
//changed later (but malloc's arg was not)
2. malloc() does not handle allocation failures, so you have to test the return value of malloc() on each and every call. This tedious and dangerous function imposes performance penalty and bloats your .exe files. On the other hand, new throws an exception of type std::bad_alloc when it fails, so your code may contain only one catch(std::bad_alloc) clause to handle such exceptions.
3. As opposed to new, malloc() does not invoke the object's constructor. It only allocates uninitialized memory. The use of objects allocated this way is undefined and should never occur. Similarly, free() does not invoke its object's destructor.
18. Placement-New Requires Heap-Allocated Buffers
The placement-new operator constructs an object on a pre-allocated buffer. The pre-allocated buffer has to be allocated on the heap.

char *pbuff = new char[1024]; // heap allocation using plain new
Person *p = new (pbuff) Person; // placement new uses a pre-allocated buffer
You may be tempted to use a buffer allocated on the stack to avoid the need of explicitly freeing it:
char pbuff [1024]; //bad idea: stack allocation instead of heap
Person *p = new ( pbuff ) Person; //undefined behavior
However, the pre-allocated buffer must comply with certain alignment constraints required by C++. Memory allocated on the heap is guaranteed to comply with these requirements. Stack memory, on the other hand, may not be properly aligned, and hence using it for that purpose yields undefined behavior.
19. Using "Sentry Bytes" to Detect Memory Overruns
One of the most frustrating aspects of debugging is detecting memory overruns. Some compilers (MS Visual C++ for instance) automatically add "sentry bytes" at the ends of every memory block allocated by operator new. The sentry bytes have a predefined value that the runtime system monitors. When the predefined value changes, this indicates that the program wrote to an out of bound memory address. Unfortunately, compilers apply this technique to heap memory exclusively. To detect memory overruns in stack memory, you can implement this technique manually. This code snippet writes one position beyond the last element of the array:

void erratic()
{
int arr[2];
for (int j= 0; j<3; j++) //this loop iterates one to many times
arr[j] = j;
}
In its last iteration, the loop writes the value 2 to a memory address that is beyond the valid array bounds. This version of the previous code snippet uses two initialized sentry integers that are declared immediately before and after the original array. The values of the sentry integers are then examined to detect the memory overrun:

void erratic()
{
#ifdef DEBUG
int before_sentry = -99;

No comments: