Print service provided by iDogiCat: http://www.idogicat.com/
home logo





Home > IT > Programming > C++ Programming > iDog's Interview C++ > iDog's Interview C++ Part 8

iDog's Interview C++ Part 8

Templates, Exceptions, Cast

Format of templates

Template class:


template <class T>
class MyTmplClass
{
private:
    T* m_pt;
public:
    const T* getPointerToData() const {return m_pt;}
};

The 'class' in template definition can also be replaced by keyword 'typename'. I think that this is much better since the 'class' won't make it so confusing:


template <typename T>
class MyTmplClass
{
// ...
};

Usage:


MyTmplClass<int> myIntParameterizedObj;
MyTmplClass<MyType> myMyTypeParameterizedObj;

Template function:


template <class T>
void myTmplFunc(T, int);

Usage:


MyTmplClass<int> a;
MyTmplClass<char*> b;
myTmplFunc<float>(1.0, 2);

Templates vs Macros

Templates are always prefered since they are type-safe and built into the language.

Exception Handling

Exception handling provides a type-safe, integrated method for coping with the predictable but unusual conditions that arise while running a program.

Exceptions provide an express path from the low level code where error can happen to the high level code that need to handle the error condition.


try {
    // code that can throw exceptions
    if(my_condition)  throw BaseExceptionType();
} catch(DerivedExceptionType& e) {
    // handle it
} catch(BaseExceptionType& e) {
    // handle it
} catch(AnotherException& e) {
    // re-throw it
    throw;
} catch(...) {  // all other exceptions
    // handle it
}

Note that 'throw' doesn't support throwing polymorphically:


void myThrow(BaseExceptionType& ex) {throw ex;}

// usage
DerivedExceptionType det;
try {
    myThrow(det);
} catch(DerivedExceptionType& e) {
    // ...
} catch(...) {
    // ...
}

The exception will be handled by 'catch(...)', since the exception thrown by myThrow() is of type BaseExceptionType.

Or, you can also define virtual functions in base exception class, so you only need to catch the base exception type, and it will do the right thing.

Exception is a good substitution to the traditional 'returning an error code' approach, since the latter needs more time and effort to write, debug and test.

Constructor: A constructor cannot return a value, so when there is something wrong, you should throw an exception.

Destructor: As a rule of thumb, you should avoid throwing exceptions in destructors. Reason of this is because of 'stack unwinding' machanism during processing exceptions in C++.

An explanation:

How can I handle a destructor that fails?

Write a message to a log-file. Do not throw an exception!

The C++ rule is that you must never throw an exception from a destructor that is being called during the "stack unwinding" process of another exception. For example, if someone says throw Foo(), the stack will be unwound so all the stack frames between the "throw Foo()" and the "} catch (Foo e) {" will get popped. This is called stack unwinding.

During stack unwinding, all the local objects in all those stack frames are destructed. If one of those destructors throws an exception (say it throws a Bar object), the C++ runtime system is in a no-win situation: should it ignore the Bar and end up in the "} catch (Foo e) {" where it was originally headed? Should it ignore the Foo and look for a "} catch (Bar e) {" handler? There is no good answer — either choice loses information.

So the C++ language guarantees that it will call terminate() at this point, and terminate() kills the process.

The easy way to prevent this is never throw an exception from a destructor. But if you really want to be clever, you can say never throw an exception from a destructor while processing another exception. But in this second case, you're in a difficult situation: the destructor itself needs code to handle both throwing an exception and doing "something else", and the caller has no guarantees as to what might happen when the destructor detects an error (it might throw an exception, it might do "something else"). So the whole solution is harder to write. So the easy thing to do is always do "something else". That is, never throw an exception from a destructor.

Of course the word never should be "in quotes" since there is always some situation somewhere where the rule won't hold. But certainly at least 99% of the time this is a good rule of thumb. (from C++ FAQ Lite)

Exception-safe

Grantees from standard library

  • basic grantee for all operations: basic invariants maintained, no resource leak
  • strong grantee for key operations (like push_back(), insert() of list): either succeed, or have no effects.
  • Nothrow grantee for some operations: a few simple operations like swap() and freeing memory are granteed not to throw an exception.

Good Style: RAII (Resource Acquisition Is Initialization)

Bad stype:


void myFunc() {
   int* p = new int[10];
   try {
      // use p...
   } catch(...) {
      delete[] p;  // this is redundant and tedious
      throw;
   }

   delete[] p;
}

Good style:


class MyPointer {
   int* p;
public:
   explicit MyPointer(int n) {p = new int[n];}
   ~MyPointer() {delete[] p;}
   operator int*() {return p;}
};

void myNewFunc() {
   MyPointer mp(10);
   // use mp...
}

Exception-safe

Bad example:


Array& Array::operator=(const Array& rhs) {
   if(this == &rhs)  return *this;

   m_size = rhs.m_size;
   delete[] m_data;
   m_data = new int[m_size];
   copy(rhs.m_data, rhs.m_data + m_size, m_data);
}

// usage
try {
   Array v1(10);
   Array v2(HUGE);
   v1 = v2;
} catch(bad_alloc) {
   cerr << "bad alloc" << endl;
}

If memory is not sufficient to allocate for m_data after the 'delete[]' operation, exception is thrown, and destructor of Array is called, which deletes m_data. But m_data has already been deleted in assignment operator, this causes the problem of deleting a pointer twice.

To solve this problem, keep it in mind that before getting a new one, don't throw away the old one.

Good example:


Array& Array::operator=(const Array& rhs) {
   if(this == &rhs)  return *this;

   int* p = new int[m_size];
   copy(rhs.m_data, rhs.m_data + m_size, p);
   delete[] m_data;      // granteed not to throw exceptions
   m_size = rhs.m_size;
   m_data = p;
}

What is "generic programming" and what's so great about it?

Generic programming is programming based on parameterization: You can parameterize a type with another (such as a vector with its element types) and an algorithm with another (such as a sort function with a comparison function). The aim of generic programming is to generalize a useful algorithm or data structure to its most general and useful form. For example, a vector of integers is fine and so is a function that finds the largest value in a vector of integers. However, a generic solution that provides a vector of any type the user cares to use and a function that finds the largest value in any vector is better still:

vector::iterator p = find(vs.begin(), vs.end(), "Grail");

vector::iterator q = find(vi.begin(), vi.end(), 42);

These examples are from the STL (the containers and algorithms part of the ISO C++ standard library); for a brief introduction, see A Tour of the Standard Library from TC++PL.

Generic programming is in some ways more flexible than object-oriented programming. In particular, it does not depend on hierarchies. For example, there is no hierarchical relationship between an int and a string. Generic programming is generally more structured than OOP; in fact, a common term used to describe generic programming is "parametric polymorphism", with "ad hoc polymorphism" being the corresponding term for object-oriented programming. In the context of C++, generic programming resolves all names at compile time; it does not involve dynamic (run-time) dispatch. This has led generic programming to become dominant in areas where run-time performance is important.

Please note that generic programming is not a panacea. There are many parts of a program that need no parameterization and many examples where run-time dispatch (OOP) is needed.

Cast

C style cast obsoleted:


int i = (int)floatVariable;

C++ function call style:


int i = int(floatVariable);

New standard: Four cast operators:

  • static cast
  • const cast
  • dynamic cast
  • reinterpret cast

The dynamic_cast<>() operator is used to convert a "base class pointer/reference" to a "derived class pointer/reference". Done at run-time, so the condition of the operation must be checked. When a dynamic cast of a pointer fails, it returns NULL; when a dynamic cast of a reference fails, it throws exception std::bad_cast (declared in <typeinfo>).

Casts from a base class reference or pointer to a derived class reference or pointer are called downcasts. When this happen, usually it means that there are some problem in the design.


Base* pb;
Derived* pd;
Derived d;
pb = &d;
if(pd = dynamic_cast<Derived*>(pb)) {
    // do something
}

Differences between dynamic_cast & reinterpret_cast

  • Argument they takes: reinterpret_cast only takes pointers as arguments; dynamic_cast takes both pointers and references
  • when casting pointers, they also have differences:
    • Provide safeguard or not: reinterpret_cast provides no safeguard, it just do the cast, no matter it makes sense or not; dynamic_cast fails if the requested type doesn't match actual type.
    • Purpose: reinterpre_cast can be used to cast from any pointer to any other pointer; dynamic_cast can only be used for downcasting to derived classes with at least one virtual member function.

A common usage of reinterpret_cast operator is to obtaining the individual bytes of a certain type (eg.: to reinterpret an int variable to char*, to get each of the sizeof(int) bytes of it). When using a reinterpret_cast the compiler offers absolutely no safeguard. The compiler will happily reinterpret_cast an int* to a double*, for example.

The dynamic_cast will also reinterpret a block of memory as something else, but here a run-time safeguard is offered. The dynamic cast fails when the requested type doesn't match the actual type of the object we're pointing at. The dynamic_cast's purpose is also much more restricted than the reinterpret_cast's purpose, as it should only be used for downcasting to derived classes having virtual members.

typeid operator

As with the dynamic_cast<>() operator, the typeid is usually applied to base class objects, that are actually derived class objects. Similarly, the base class should contain one or more virtual functions.

In order to use the typeid operator, source files must include following header file:


#include <typeinfo>