This is an old revision of the document!


$conf['maxtoclevel'] = 3;


This page is based on the excellent books 50 ways to improve your C++ and 35 more ways to improve your C++.

Effective C++

Shifting from C to C++

Item 1 : Use const and inline instead of #define

Because constants don't appear in the symbole table which is annoying for debugging :

const float ASPECT_RATIO = 1.653;

And because there are lot of traps with macros :

template<class T> inline T& MAX(T& a, T& b) { return a>b ? a : b; }

Item 2 : Prefer iostream to stdio.h

Because of type safety an extensiblity. To define stream operators in your class :

friend ostream& operator<<(ostream& s, const ComplexInt& c);
ostream& operator<<(ostream& s, const ComplexInt& c) { s << c.r << " " << c.i; return s; }

Item 3 : Use new and delete instead of malloc and free

Because they don't call constructors and destructors !

Item 4 : Prefer C++-style comments

Because you can't embed C-style comments in other comments !

Memory management

Item 5 : Use the same form in corresponding calls to new and delete

new with delete, and new[] with delete[], or memory leaks ! (don't call all destructors)

Item 6 : Call delete on pointer members in destructors

Initialize all pointer members in each of the constructors (at NULL if not allocated) ;
Delete for all pointer members the existing memory and assign new memory in the assignment operator ;
Delete the memory in the destructor

Item 7 : Check the return value of new

Set an error-handling function, globally :

void noMoreMemory() { cerr << "Unable to satisfy request for memory\n"; abort(); }
main() { set_new_handler(noMoreMemory); ... }

Or for a specific class :

typedef void (*PEHF)(); //  PEHF = pointer to error handling function
 
class X
{
 private:
 	static PEHF currentPEHF;
 public:
	static PEHF set_new_handler(PEHF p);
	void * operator new(size_t size);
};
 
PEHF X::currentPEHF; //sets currentPEHF to 0 by default
 
PEHF X::set_new_handler(PEHF p)
{
	PEHF oldPEHF = currentPEHF;
	currentPEHF = p;
	return oldPEHF;
}
 
void * X::operator new(size_t size)
{
	PEHF currentHandler = ::set_new_handler(currentPEHF);
	void *memory = ::new char[size];
	::set_new_handler(currentHandler);
	return memory;
}

Item 8 : Adhere to convention when writing new

Means having the right return value (pointer or 0), and calling an error-handling function when insufficient memory is avalaible :

void * operator new(size_t size) // your operator new could take additional params
{
	while(true)
	{
		// HERE attempt to allocate size bytes
		if (the allocation was sucessful)
			return (a pointer to the memory);
		PEHF currentHandler = set_new_handler(0); // get the current ...
		set_new_handler(currentHandler);	// ... error handling function
		if (currentHandler) (*currentHandler)(); else return 0;
	}
}

If the operator new is in a class X, you should add before to attempt to allocate memory :

if (size != sizeof(X)) return ::new char[size];

This occur when you inheritate from the class without rewriting the new operator.

Item 9 : Avoid hiding the global new

If you add parameters to your new redefinition, it blocks access to the usual form of new, so rewrite also the classical form :

class X
{
	void * operator new(size_t size, PEHF pehf);
	void * operator new(size_t size) { return ::new char[size]; }
};
 
void specialErrorHandler();
 
X *px1 = new(specialErrorHandler) X;
X *px2 = new X; // doesn't work if you don't define new(size_t)

Item 10 : Write delete if you write new

You can rewrite new to allocate small objects in a large memory zone (in order to speed up allocations and save memory), but you have also to write delete !

class Airplane
{
 private:
 	Airplane *rep;
 	static Airplane *headOfFreeList;
 public:
 	void * operator new(size_t, size);
 	void operator delete(void *deadObject, size_t size);
};
 
Airplane *Airplane::headOfFreeList; // initialized to 0 by default
 
void * Airplane::operator new(size_t size)
{
	if (size != sizeof(Airplane)) return ::new char[size];
	Airplane *p = headOfFreeList;
	if (p) headOfFreeList = (Airplane*) p->rep;
	else
	{
		Airplane *newBlock = (Airplane*) ::new char[256 * sizeof(Airplane)]; // don't call constructor !
		if (newBlock == 0) return 0;
		for(int i  0; i < 255; i++) // link the memory chunks together
			newBlock[i].rep = (Airplane*) &newBlock[i+1];
		newBlock[255].rep = 0;
		p = newBlock;
		headOfFreeList = &newBlock[1];
	}
	return p;
}
 
void Airplane::operator delete(void *deadObject, size_t size)
{
	if (size != sizeof(Airplane)) { ::delete[] ((char*) deadObject); return; }
	Airplane *carcass = (Airplane*) deadObject;
	carcass->rep = (AirplaneRep*) headOfFreeList;
	headOfFreeList = carcass;
}

Constructors, Destructors, and Assignment operators

Item 11 : Define a copy constructor and an assignment operator for classes with dynamically allocated memory

Item 12 : Prefer initialization to assignment in constructors

You don't have choice for constants or reference members, but in general it is more efficient (constructors are always called once, and with initialization all members are written in raw which is faster than assigning them one by one).

Item 13 : List members in an initialization list in the order in which they are declared

Because it is the order of the declarations which is followed by the compiler, not the order of the initialization list, so you can fall into traps if you don't have the same order.

Item 14 : Make destructors virtual in base classes

Because if you don't, if you create a derived object and put it in a pointer of the base class, when you will delete it it won't call the destructor of the derived class ! But only for base class (if you plan to inheritate from it), which generally corresponds to the fact that there is at least one virtual function, because you loose a little performance. Tip : if you want to have an abstract class, but you don't have any function to make pure virtual, declare a pure virtual destructor ! (but you have nevertheless to provide a definition for this pure virtual destructor, and do not declare it inline because it could have problems with the virtual thing).

Item 15 : Have operator= return a reference to *this

To allow chains assignments :

C& C::operator=(const C&);

Item 16 : Assign to all data members in operator=

Because if you want to assign some, the compiler won't anymore assign the other ones. Moreover if it is a derived class you have also to initialize the data members of the base class :

((A&) *this = rhs;

(if base class doesn't provide = operator)

A::operator=(rhs);

(if it does)

Item 17 : Check for assignment to self in operator=

That's to say, always begin by : if (this == &rhs) return *this;. Not only because it saves time, but above all because it will create serious problems with freeing and reallocating resources ! But be careful this test won't work with multiple inheritance because one same object can have different addresses according to the type it is casted to. If so you could add an unique identifier for each object.

Classes and Functions : Design and Declaration

Item 18 : Strive for class interfaces that are complete and minimal

Item 19 : Differentiate among member functions, global functions, and friend functions

Item 20 : Avoid data members in the public interface

Item 21 : Use const whenever possible

Item 22 :

Item 23 :

Item 24 :

Item 25 :

Item 26 :

Item 27 :

Item 28 :

Item 29 :

Item 30 :

Item 31 :

Item 32 :

Item 33 :

Item 34 :

Item 35 :

Item 36 :

Item 37 :

Item 38 :

Item 39 :

Item 40 :

Item 41 :

Item 42 :

Item 43 :

Item 44 :

Item 45 :

Item 46 :

Item 47 :

Item 48 :

Item 49 :

Item 50 :

More effective C++

Basics

Item 1 : Distinguish between pointers and references

References must refer to an object (no null reference), and that's why they must be initialized. Indeed you don't have to check if a reference parameter is null or not, contrary to pointers. But references can't be reassigned to refer to different objects.

Item 2 : Prefer C++-style casts

Because they are easier to parse (both for humans and tools), and because it can avoid errors.
static_cast<type>(expression) : normal cast, for example double to int.
const_cast to cast away the constness or volatileness of an expression :

void update(X *px);
const X x;
update(const_cast<X*>(&x));

dynamic_cast to perform safe casts down or across an inheritance hierarchy (base to derived objects), and it returns NULL if it fails (or throw an exception when casting references).
reinterpret_cast for implementation-defined casts (rarely portable), for example casting between function pointer types :

typedef void (*FuncPtr)(); // a FuncPtr is a pointer to "void foo()"
FuncPtr funcPtrArray[10];
int doSomething();
funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething); // can work, but can yield incorrect results too !

Item 3 : Never treat arrays polymorphically

That's to say don't place derived class objects in an array of base class pointers, if they don't have the same size, because indexing will use size of the base class. So problems when you write your loop, or when you delete the array (the compiler generates a loop, and it is said in the language specification that the result is undefined).

Item 4 :

Operators

Item 5 :

Item 6 :

Item 7 :

Item 8 :

Exceptions

Item 9 :

Item 10 :

Item 11 :

Item 12 :

Item 13 :

Item 14 :

Item 15 :

Efficiency

Item 16 :

Item 17 :

Item 18 :

Item 19 :

Item 20 :

Item 21 :

Item 22 :

Item 23 :

Item 24 : Understand the costs of virtual functions, multiple inheritance, virtual base classes, and RTTI

Virtual functions works with virtual tables (one per class) containing pointers to functions, and virtual table pointers (one per object) pointing to the good virtual table. Hence it increases size of objects, per-class data, and reduce performance : because of indirections (but that's almost nothing), and mainly because it prevents inlining (except if the function is called from an object and not a pointer or a reference, but that's almost never the case).

Multiple inheritance leads to more per-class data (special virtual tables must be generated for base classes), increases size of objects (multiple virtual table pointers within a single object), and the runtime invocation cost of virtual function grows slightly (offset of virtual table pointers are more complicated to calculate).

Moreover if you declare base classes virtual (what you must do to avoid data replication if there are more than one inheritance path to a base class), it increases size of objects by adding several pointers to virtual base class.

RTTI (RunTime Type Identification) stores a type_info object in the virtual table (so it only works if there are virtual functions in the class, and increases a little the per-class data), and the typeid operator let us discover informations about objects at runtime.

To conclude it is important to understand the costs of theses functionalities, but also to understand that if we need it we will pay for it, so there is no point in trying to emulate it by another way. But you can have legitimate reasons to bypass the compiler-generated services for example because pointers to virtual tables can make it difficult to store C++ objects in databases or to move them across process boundaries, but it will be less efficient !

Techniques

programming/improve-your-cpp.1166107622.txt.gz · Last modified: 2013/09/19 16:43 (external edit)
CC Attribution-Share Alike 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0