I'm writing a C++ program that makes use of a C library. The C library has a logging system with a macro that looks like that at its core:
#define MACRO(s, ...) printf("log: " s "\n", ##__VA_ARGS__)
// supposed to be called like:
MACRO("network error %d", errno)
As you can see, the first argument of the macro can only be a string literal, because the expansion will paste the two literals together. The following wouldn't work:
char msg[] = "network error %d";
MACRO(msg, errno); // syntax error!
I wanted to write a function that wraps some repetitive work I was doing (check the error code, if an error, log it and throw. But I wasn't able to get something much nicer than the following:
#include <stdexcept>
#include <cstdarg>
#include <cstdio>
void throw_on_err(err_t err, const char *format...) {
if (err != OK) {
char buffer[128];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
MACRO("%s", buffer);
throw std::runtime_error(buffer);
}
}
// supposed to be called like:
throw_on_err(errno, "network error");
But this has a couple of issues: it artificially limits the error message size, and the work of (vsn)printf
is done twice.
Now, I know I could replace throw_on_err
with a macro, but I'd like to avoid that solution: it's harder to debug, and type safety is poorer. I want to use the library's macro, because it's standard and does more work than what I've shown (work that isn't relevant for the problem at hand, like adding color to the output, checking the log level, etc). I tried to juggle with const char*
params, constexpr
functions, but to no avail.
Honestly, I'd just do this:
void throw_on_err(int err, const std::string& message)
{
if (err != 0)
{
MACRO("%s", message.c_str());
throw std::runtime_error(message);
}
}
And that easily facilitates your basic case:
throw_on_err(errno, "network error");
And it also works if someone wants to jam additional arguments into it by just doing inline string concatenation.
const char* reason = getLastError();
throw_on_err(errno, std::string("network error: ") + reason);
If you got to have variadic arguments for your log function, consider moving away printf style variading args with a leading format string. And consider just having a variadic template function declared in a header file:
#include <sstream>
#include <string>
template <typename T>
void throw_on_err_impl(std::ostringstream& ss, int argIndex, const T& t)
{
if (argIndex > 0)
{
ss << " ";
}
ss << t;
MACRO("%s", ss.str().c_str());
throw std::runtime_error(ss.str());
}
template<typename T, typename... Rest>
void throw_on_err_impl(std::ostringstream& ss, int argIndex, const T& t, Rest... rest)
{
if (argIndex > 0)
{
ss << " ";
}
ss << t;
throw_on_err_impl(ss, argIndex+1, rest...);
}
template<typename... Args>
void throw_on_err(int errorCode, Args... args)
{
if (errorCode == 0)
{
return;
}
std::ostringstream ss;
throw_on_err_impl(ss, 0, args...);
}
// this single parameter overload is optional.
inline void throw_on_err(int errorCode)
{
throw_on_err(errorCode, "generic error");
}
Then the above can be invoked in any sort of ways:
Simply as:
throw_on_err(errno, "network error");
Or something like this:
std::string host = "www.stackoverflow.com";
std::string internalErrorReason = "tls handshake failure";
int internalStep = 9;
throw_on_err(errno, "network error:", internalErrorReason, "internal step:", internalStep);