I had a similar issue: how does one enrich an exception with contextual information ?
Boost proposes one solution: try/catch
and enrich the exception in the catch
block before rethrowing it. It does enrich the exception, but not automatically.
The solution I came up with in the end is extremely simple, and based on C++ destructors strength. But first, how to use it:
void foo(int i) {
LOG_EX_VAR(i);
// do something that might throw
}
Yep, that's all, a single macro call and i
is added to an exception context along with the function name, file name and line number the macro was expanded at.
What's behind ? The most important const and a bit of magic.
class LogVar {
public:
LogVar(LogVar const&) = delete;
LogVar& operator=(LogVar const&) = delete;
virtual ~LogVar() {}
protected:
LogVar();
}; // class LogVar
template <typename T>
class LogVarT: public LogVar {
public:
LogVarT(char const* fc, char const* fl, int l, char const* n, T const& t):
_function(fc), _filename(fl), _line(l), _name(n), _t(t) {}
~LogVar() {
ContextInterface::AddVariable(_function, _filename, _line, _name, _t);
}
private:
char const* _function;
char const* _filename;
int _line;
char const* _name;
T const& _t;
}; // class LogVarT
template <typename T>
LogVarT make_log_var(char const* fc,
char const* fl,
int l,
char const* n,
T const& t)
{
return LogVarT(fc, fl, l, n, t);
}
#define LOG_EX_VAR(Var_) \
LogVar const& BOOST_PP_CAT(_5416454614, Var_) = \
make_log_var(__func__, __FILE__, __LINE__, #Var_, Var_);
This works fairly well, if you can get the difficult part (the ContextInterface::AddVariable()
function) to work.
If you don't want to bother with it, then go for a thread_local
std::vector<LogVar*>
as you did. Just be aware that you'll be doing a lot of work for nothing.
If you are interested in it, then follow on.
- Let's realize that the most important part here is to get something that is thread-safe. So the context will be global... per thread (aka
thread_local
). But even then one might accidentally leak a reference to it outside.
- It is important to realize that several exceptions may coexist, though only one is uncaught at any point in time: that is an exception may be thrown within a
catch
clause.
- We can only instrument the exceptions we throw ourselves, so we need some kind of default policy for the others. Logging, for example.
So, let's get the interface straight:
class ContextInterface {
public:
typedef std::unique_ptr<ContextInterface> UPtr;
typedef std::shared_ptr<ContextInterface> SPtr;
typedef std::weak_ptr<ContextInterface> WPtr;
static UPtr SetDefault(UPtr d) {
std::swap(d, DefaultContext);
return d;
}
template <typename T, typename... Args>
static SPtr SetActive(Args&&... args) {
SPtr ci = ExceptionContext.lock();
if (ci.get()) { return ci; }
ci.reset(new T(std::forward<Args>(args)...));
ExceptionContext = ci;
return ci;
}
template <typename T>
static void AddVariable(char const* fc,
char const* fl,
int l,
char const* n,
T const& t)
{
SPtr sp = ExceptionContext.lock();
ContextInterface* ci = sp.get();
if (not ci) { ci = DefaultContext.get(); }
if (not ci) { return; }
ci->report(fc, fl, l, n) << t;
}
virtual ~ContextInterface() {}
private:
static thread_local UPtr DefaultContext;
static thread_local WPtr ExceptionContext;
virtual std::ostream& report(char const* fc,
char const* fl,
int l,
char const* n) = 0;
}; // class ContextInterface
And finally, the last missing piece (well, apart from the actual contexts you would want I guess): an example of the base exception class.
class ContextualException: public virtual std::exception {
public:
ContextualException(): _c(ContextInterface::SetActive<ExceptionContext>()) {}
ContextInterface const& context() const { return *_c; }
private:
ContextInterface::SPtr _c;
}; // class ContextualException