views:

68

answers:

2

I've got this logging system for which I'm looking to shortcut some of the string manipulation.

The logging system is used via functional macros which then forward to a single function call. E.g. #define Warning(...) LogMessage(eWarning, __VA_ARGS__);.

LogMessage then does a snprintf into a new buffer and then presents that message to whatever log targets happen to be installed; printf, OutputDebugString, etc.

Unfortunately, I've run into a problem where the buffer that we have isn't big enough, so the output gets truncated. I also realized that this method will fail if the output message has percent symbols in it, as snprintf will try to process the va_args. Finally, as the majority of our log messages do not use the va_args, it seems silly to copy the string just to present it to the loggers.

So- given my function prototype, should I be able to overload based on the presence of the ellipses? In other words, should I be able to assume that I can do something like:

LogMessage(LogLevel, const char* message, ...);
LogMessage(LogLevel, const char* message);

My google attempts haven't yielded anything particularly useful (just showing me that ellipses will match if nothing else does, varying from my requirements that nothing matches), and my initial stab at an implementation just gave me an ambiguous function call error.

With the error, I should just accept that I can't do this, but I'm wondering if it's just the compiler I'm using or if maybe I'm doing it wrong. I can achieve a similar effect with

// edited version of what I really have to remove our local APIs,
// please excuse minor errors
const char* message = NULL;
char buffer[512];

va_list args;
va_start(args, format);

if(strcmp(format, "%s") == 0) {
    message = va_arg(args, const char*);
}
else if (strchr(format, '%') == NULL) {
    message = format;
}
else {
    vsnprintf(buffer, 512, format, args);
    message = buffer;
}

va_end(args);

...but this seems wasteful in the typical case which can be known simply by the number of parameters being passed. E.g. if ellipses don't match anything, select the other function? If this doesn't work, is there another method I can try that doesn't require the user to decide with the macro name which function will be called? Honestly it's not even as much about the "waste" once I realized that if someone haphazardly said Error("Buffer not 100% full"); in their log message and got "Buffer not 1007.732873e10ull" as a result.

Edit: While my example has been answered by a "don't do that," can the question itself be answered?

+3  A: 

I also realized that this method will fail if the output message has percent symbols in it, as snprintf will try to process the va_args.

Then caller beware. If your function is documented to take printf-style format strings then it is the caller's responsibility to escape any percent signs. It is not really your job to attempt to handle invalid format strings.

Honestly it's not even as much about the "waste" once I realized that if someone haphazardly said Error("Buffer not 100% full"); in their log message and got "Buffer not 1007.732873e10ull" as a result.

I think you're better off going along with the C++ ethos. In Java methods commonly check for valid arguments and throw exceptions when passed invalid values. In C++ you simply let callers shoot themselves in the foot. It's better to have them write 100%% than to jump through hoops to protect them from learning how to call your function properly.

John Kugelman
I guess I don't buy that philosophy, preferring to be liberal in what I accept and conservative in what I send. IMO, if I can divine a user's intent, I should. Software failing in the field simply due to inexperienced programmers is inexcusable to me. And my point was expressly that I don't want to document these functions as accepting printf format strings *only*, I want them to go either way.
dash-tom-bang
As soon as you allow printf-style arguments, you *must* assume that that's what they are when you see them, otherwise those who want to use them are not going to get the behavior you expect. It's just not going to work to try and mix whether you process printf-style arguments as printf-style arguments. It's just asking for trouble. It's fine to check whether the string is valid for sprintf or not, but don't expect it to work cleanly to accept printf-style arguments and ignore them at the same time.
Jonathan M Davis
And considering how many problems web developers have because browsers accept bad html, I don't buy for a second that accepting liberally but being conservative in what you send is a good idea. We'd all be better off if the web hadn't been built with that philosophy. Don't do it in your own code if you can avoid it. Sure, program in a way that mitigates errors (even for newbies), but don't sacrifice precision in the vain hope of it working better. It won't.
Jonathan M Davis
I can accept that argument, although the class of software that I'm working on is substantially different (embedded systems that only communicate with each other). I suppose my workaround above follows the principle of least surprise even if it'd be nice (wrt simplifying code) to have an actually different code path for the no varargs case.
dash-tom-bang
A: 

Ok I think I came up with a solution for the question.

The fact is that you can't overload based only on whether there are parameters for the ellipses or not. I.e. you can't have functions which have signatures that vary only on the presence of the ellipses.

However, it is possible to do something like what I was asking if I drop the const char* parameter from the ellipses prototype. I.e.

LogMessage(LogLevel, ...);
LogMessage(LogLevel, const char* message);

is unambiguous, but now you battle with the fact that you have to assume that the first parameter is a const char*, but it may well not be. Taking John Kugelman's advice, maybe that's fine; you document the parameters that are allowed and user beware. The non-ellipses function will be called if there is only a const char*, and the ellipses function will be called if there is any thing else including the documented const char* followed by some number of parameters.

Unfortunately it seems that this is the extent of a possible solution that allows you to pass the va_args on to child functions, in my example case to vsnprintf.

It's probably bad form to accept my own answer, even though it's the one that answers the presented question.

dash-tom-bang