I've tried all alternatives to exceptions I could find (member error variable, even setjmp/longjmp), and they all sucked in their own special way. A very uncommon pattern which I've come to love is passing a reference to an error object around, and checking if an error is pending as the very first operation in any function:
int function1(Error& e, char * arg)
{
if(e.failure())
return -1; // a legal, but invalid value
// ...
}
int function2(Error& e, int arg)
{
if(e.failure())
return -1; // a legal, but invalid value
// ...
}
int function3(Error& e, char * arg)
{
if(e.failure())
return -1;
// if function1 fails:
// * function2 will ignore the invalid value returned
// * the error will cascade, making function2 "fail" as well
// * the error will cascade, making function3 "fail" as well
return function2(e, function1(e, arg));
}
With some work, it applies to constructors as well:
class Base1
{
protected:
Base1(Error& e)
{
if(e.failure())
return;
// ...
}
// ...
};
class Base2
{
protected:
Base2(Error& e)
{
if(e.failure())
return;
// ...
}
// ...
};
class Derived: public Base1, public Base2
{
public:
Derived(Error& e): Base1(e), Base2(e)
{
if(e.failure())
return;
...
}
};
The main issue is you don't get automatic deletion in case the constructor of a dynamically allocated object fails. I typically wrap invocations of new in a function like this:
// yes, of course we need to wrap operator new too
void * operator new(Error& e, size_t n)
{
if(e.failure())
return NULL;
void * p = ::operator new(n, std::nothrow_t());
if(p == NULL)
/* set e to "out of memory" error */;
return p;
}
template<class T> T * guard_new(Error& e, T * p)
{
if(e.failure())
{
delete p;
return NULL;
}
return p;
}
Which would be used like this:
Derived o = guard_new(e, new(e) Derived(e));
Advantages of this technique include:
- interoperability with C (if the Error class is declared appropriately)
- thread-safe
- zero size overhead in classes
- the Error class can be 100% opaque; with the use of macros to access, declare and pass it around, it can include all sorts of information, including but not limited to source file and line, function name, stack backtrace, etc.
- it applies to pretty complex expressions, making it almost like exceptions in many cases