Principles of Custom Exception Types
C++ allows exception types to be user-defined classes. When an exception is thrown, the runtime attempts to match the type in a top-down manner using strict type checikng. However, the assignment compatibility rule applies: a base class handler can catch exceptions of derived classes.
To ensure correct behavior:
- Place
catchblocks for derived (child) exceptions before those for base (parent) exceptions. - A well-designed exception class hierarchy is fundamental infrastructure for modern C++ libraries.
Base Exception Interface
Define an abstract base class BaseError with a pure virtual destructor. Common initialization logic is extracted into a helper function.
Header definition:
class BaseError
{
protected:
char* m_desc;
char* m_origin;
void prepare(const char* desc, const char* srcFile, int srcLine);
public:
BaseError(const char* desc);
BaseError(const char* srcFile, int srcLine);
BaseError(const char* desc, const char* srcFile, int srcLine);
BaseError(const BaseError& rhs);
BaseError& operator=(const BaseError& rhs);
virtual const char* description() const;
virtual const char* origin() const;
virtual ~BaseError() = 0;
};
Implementation:
void BaseError::prepare(const char* desc, const char* srcFile, int srcLine)
{
m_desc = strdup(desc ? desc : "");
if (srcFile)
{
char lineBuf[16] = {0};
itoa(srcLine, lineBuf, 10);
size_t len = strlen(srcFile) + strlen(lineBuf) + 3;
m_origin = static_cast<char*>(malloc(len));
m_origin[0] = '\0';
strcat(m_origin, srcFile);
strcat(m_origin, ":");
strcat(m_origin, lineBuf);
}
else
{
m_origin = nullptr;
}
}
BaseError::BaseError(const char* desc)
{
prepare(desc, nullptr, 0);
}
BaseError::BaseError(const char* srcFile, int srcLine)
{
prepare(nullptr, srcFile, srcLine);
}
BaseError::BaseError(const char* desc, const char* srcFile, int srcLine)
{
prepare(desc, srcFile, srcLine);
}
BaseError::BaseError(const BaseError& rhs)
{
m_desc = strdup(rhs.m_desc);
m_origin = rhs.m_origin ? strdup(rhs.m_origin) : nullptr;
}
BaseError& BaseError::operator=(const BaseError& rhs)
{
if (this != &rhs)
{
free(m_desc);
free(m_origin);
m_desc = strdup(rhs.m_desc);
m_origin = rhs.m_origin ? strdup(rhs.m_origin) : nullptr;
}
return *this;
}
const char* BaseError::description() const
{
return m_desc;
}
const char* BaseError::origin() const
{
return m_origin;
}
BaseError::~BaseError()
{
free(m_desc);
free(m_origin);
}
The description() and origin() methods are const to allow inspection via const BaseError& references in catch blocks without risk of modification.
Derived Exception Classes
Specific exceptions inherit from BaseError and forward constructor arguments.
class MathError : public BaseError
{
public:
MathError() : BaseError("") {}
MathError(const char* desc) : BaseError(desc) {}
MathError(const char* file, int line) : BaseError(file, line) {}
MathError(const char* desc, const char* file, int line) : BaseError(desc, file, line) {}
MathError(const MathError& rhs) : BaseError(rhs) {}
MathError& operator=(const MathError& rhs)
{
BaseError::operator=(rhs);
return *this;
}
};
class NullPtrError : public BaseError
{
public:
NullPtrError() : BaseError("") {}
NullPtrError(const char* desc) : BaseError(desc) {}
NullPtrError(const char* file, int line) : BaseError(file, line) {}
NullPtrError(const char* desc, const char* file, int line) : BaseError(desc, file, line) {}
NullPtrError(const NullPtrError& rhs) : BaseError(rhs) {}
NullPtrError& operator=(const NullPtrError& rhs)
{
BaseError::operator=(rhs);
return *this;
}
};
class BoundsError : public BaseError
{
public:
BoundsError() : BaseError("") {}
BoundsError(const char* desc) : BaseError(desc) {}
BoundsError(const char* file, int line) : BaseError(file, line) {}
BoundsError(const char* desc, const char* file, int line) : BaseError(desc, file, line) {}
BoundsError(const BoundsError& rhs) : BaseError(rhs) {}
BoundsError& operator=(const BoundsError& rhs)
{
BaseError::operator=(rhs);
return *this;
}
};
class MemoryError : public BaseError
{
public:
MemoryError() : BaseError("") {}
MemoryError(const char* desc) : BaseError(desc) {}
MemoryError(const char* file, int line) : BaseError(file, line) {}
MemoryError(const char* desc, const char* file, int line) : BaseError(desc, file, line) {}
MemoryError(const MemoryError& rhs) : BaseError(rhs) {}
MemoryError& operator=(const MemoryError& rhs)
{
BaseError::operator=(rhs);
return *this;
}
};
class ParamError : public BaseError
{
public:
ParamError() : BaseError("") {}
ParamError(const char* desc) : BaseError(desc) {}
ParamError(const char* file, int line) : BaseError(file, line) {}
ParamError(const char* desc, const char* file, int line) : BaseError(desc, file, line) {}
ParamError(const ParamError& rhs) : BaseError(rhs) {}
ParamError& operator=(const ParamError& rhs)
{
BaseError::operator=(rhs);
return *this;
}
};
Utility Macro and Usage
A macro simplifies throwing exceptions with file and line information:
#define RAISE_ERROR(cls, msg) throw cls(msg, __FILE__, __LINE__)
Example test:
#include <iostream>
#include "BaseError.h"
int main()
{
try
{
RAISE_ERROR(BoundsError, "Index out of range");
}
catch (const BoundsError& e)
{
std::cout << "Caught BoundsError\n";
std::cout << e.description() << std::endl;
std::cout << e.origin() << std::endl;
}
catch (const BaseError& e)
{
std::cout << "Caught BaseError\n";
std::cout << e.description() << std::endl;
std::cout << e.origin() << std::endl;
}
return 0;
}
Always order catch blocks from most derived to most base to ensure correct matching.