Saturday, May 31, 2008

C++ #16

51. Data Pointers vs. Function Pointers
C and C++ make a clear-cut distinction between two types of pointers: data pointers and function pointers. A function pointer embodies several constituents such as the list of arguments, a return-to address, and the machine instructions. A data pointer, on the other hand, merely holds the address of the first byte of a variable. The substantial difference between the two functions led the C standardization committee to prohibit the use of void* (or data pointers of any kind) to represent function pointers and vice versa. In C++ this restriction was relaxed, yet the results of coercing a function pointer to a void * are implementation-dependent. The opposite (converting of data pointers to function pointers) is a recipe for a core dump. Also, unlike a data pointer, a function pointer cannot be dereferenced
52. Passing More Arguments to a Callback Function
Callback functions have a fixed signature so you cannot alter the number or type of the arguments it takes. For example, the standard qsort() function takes a pointer to a function that has the following signature:

int (cmp*)(const void *, const void *) //user's comparison function

This signature allows you to pass exactly two arguments to your custom-made comparison function. However, suppose that the comparison function has to compare strings and you want to pass a third argument to indicate whether the comparison should be case-sensitive or not. You cannot simply add a third argument to the function call, because the compiler will complain about it (don't even think about casting the function pointer; see this tip). Instead, you can define a struct that contains two members:

struct Comparison
{
char *name;
bool caseSensitive;
};
Now instead of passing a pointer to char as the second argument, you can pass a pointer to an instance of that struct. The comparison function will unpack the struct and compare the strings accordingly:

int MyCompareFunc(const void *pfirst, const void * psecond)
{
const Comparison * pcmp = (Comparison *) psecond;
const char * pstr1 = (const char *) pfirst;
const char * pstr2 = pcmp->name; //extract string from struct
if (pcmp->caseSensitive == true) //the second field of the struct
{
return strcmp(pstr1, pstr2)
}
else
{
//perform case-insensitive comparison
}
}
53. Living Without Null References
In C, algorithms that rely on pointers such as bsearch() and lfind() return a null pointer to indicate that the sought after element wasn't found. Unlike a pointer, a reference must always be bound to a valid object. Therefore, a reference can never be null. I've often seen the following dreadful hack as an attempt to fabricate a null reference:

int& locate(int * pi, int size, int value)
{
if (find_int(pi, size, value) != NULL)
//…
else
return *((int *) (0)); //very bad
}
The return statement fakes a null reference by binding a reference to a dereferenced null pointer. Some compilers don't detect this hack and compile this code silently, letting the users detects its disastrous results at runtime.
This hack violates C++ rules twice: first, the function locate() returns a reference to a local automatic variable. The results of using such a reference are undefined (this is called a "dangling reference"). Worse yet, even if locate() used a non-automatic variable to avoid the dangling reference problem, any attempt to use the returned reference would still cause a runtime crash because you cannot dereference a null pointer.
There are several approaches to deal with the lack of null references. Throwing an exception is one of them. However, the overhead and complexity of exception handling may be overkill for simple functions such as locate(). In this case, you can return a reference to a special object (e.g., a negative integer when an array subscript cannot be found):

int& locate(int * pi, int size, int value)
{
static int unfound = -1;
if (find_int(pi, size, value) != NULL)
//…
else
return unfound; //fine
}
54. Invoking a Function Through a Pointer
When you call a function through a pointer to function, use that pointer as if it were the function itself, not a pointer. For example:

#include <cstring>
char buff[10];
void (*pf) (char *, const char *);
pf = strcpy; // take address of strcpy
pf(buff, "hi"); // use pf as if it were the function itself
55. Hide Function Pointer Declarations With a typedef
Can you tell what the following declaration means?

void (*p[10]) (void (*)() );
Only few programmers can tell that p is an "array of 10 pointers to a function returning void and taking a pointer to another function that returns void and takes no arguments." The cumbersome syntax is nearly indecipherable. However, you can simplify it considerably by using typedef declarations. First, declare a typedef for "pointer to a function returning void and taking no arguments" as follows:

typedef void (*pfv)();
Next, declare another typedef for "pointer to a function returning void and taking a pfv" based on the typedef we previously declared:

typedef void (*pf_taking_pfv) (pfv);
Now that we have created the pf_taking_pfv typedef as a synonym for the unwieldy "pointer to a function returning void and taking a pfv", declaring an array of 10 such pointers is a breeze:

pf_taking_pfv p[10];
56. Enumerator with Zero Value and Null Pointer Constants
Here's a topic that was recently discussed at the Standardization committee. Suppose we have the following enum type:

enum Stat { good, bad};
Is it legal to use the enumerator good, whose value is zero, as a null pointer constant? Put differently, is the following declaration allowed?

void *p = good;
After all, a null pointer constant in C++ is either 0 or 0L. The answer is "no". This declaration isn't allowed because the standard says that "a null pointer constant is an integral constant expression rvalue of integer type that evaluates to zero." Let's analyze it. A null pointer constant, e.g., NULL, 0, or 0L, must be an integral constant expression. Indeed, an enumerator is an integral constant expression. However, to qualify as a null constant expression, an identifier must also be an "rvalue of integer type". Enumerators are rvalues but they are not of integer type. Therefore, the declaration

void *p = good;
is illegal. There is another way to reach the same conclusion without resorting to C++ casuistry: C++ allows only one implicit conversion per expression. Therefore, had we used the literal zero, as in:

void *p = 0;
The compiler would have implicitly converted the literal zero to type void * and assigned it to p. However, once we use good as an initializer, two implicit conversions are necessary. The first converts the enumerator to the integer zero, and the second converts the integer zero to void *. However, because C++ allows only one implicit conversion, the following declaration is invalid:

void *p=good; // requires two implicit conversions; invalid
57. Declaring References to Functions
You can declare a reference to a function, just like you can declare a pointer to a function. For example:

void f(int n)
{
++n;
}

int main()
{
void (&rf) (int) = f; //bind rf as a reference to f()
rf(5); //call f() through its reference
}
The major difference between a pointer and a reference to a function is that the latter must always be bound to an existing function (i.e., it may not be null or dangling), and you cannot re-bind another function to it once you have initialized it.
58. Arrays of Pointers to Members

You can create an array of pointers to member just as you would create arrays of any other type. The following example creates an array of pointers to member functions of class Parrot and initializes its elements with the addresses of Parrot's member functions:

class Parrot
{
public:
void Speak() const;
void Fly() const;
};

typedef void (Parrot::*PMF)() const;
// create array
PMF funcs[2]={&Parrot::Speak, &Parrot::Fly};
59. Deleting Elements of a Container of Pointers

This is a common source of misunderstanding: a programmer creates a container of pointers, then fills that container with pointers to dynamically allocated objects. When the container is destroyed, the programmer mistakenly assumes that the objects are deleted. However, they aren't because the container contains pointers, not real objects. Consider:

vector<Base *> v;
v.push_back(new Derived);
v.clear(); // doesn't delete the object!

To properly destroy the objects of a container of pointers, you should explicitly delete its pointers before destroying the container itself:

v.push_back(new Derived);
// delete the object whose pointer is stored in v
delete v[0];
// now clear the container
v.clear();
60. auto_ptr<>: your safe guard against memory leaks
The Standard Library supplies the class template auto_ptr<> which automatically deallocates heap memory. Likewise, local objects are reclaimed in case of exiting their scope or during "stack unwinding" caused by an exception.
This technique can avoid memory leakage in the case of uncaught exception or even simplify programming by sparing the hassle of explicitly deleting every object allocated using operator new. The auto_ptr<> class template is declared in the standard <memory> header.

#include <memory> //auto_ptr<> declaration
#include <iostream>

using namespace std;

class Date{ /*...*/};

void DisplayDate()
{
//now create a local object of type auto_ptr<Date>
auto_ptr<Date> pd (new Date); //now pd is owned by the template object
cout<<pd->DateString();

//note: pd is automatically deleted by the destructor of auto_ptr; it shouldn't be deleted by programmer

}
In other words, the auto_ptr<> instance, pd, can be used like an ordinary pointer to Date but it behaves like a local object in respect to its automatic destruction.

No comments: