kofifus
kofifus

Reputation: 19276

template function returning void

I have the following function in my logging class:

template<class T> 
inline T ErrLog(T ret, const char* Format, ...)
{
    va_list args; va_start( args, Format ); _vsnprintf(mypWorkBuffer, MaxLogLength, Format, args); va_end(args);
    // do some fancy logging with mypWorkBuffer
    return ret;
}

(mypWorkBuffer is defined elsewhere)

This is a very handy shortcut to me cause I can log an error and exit in one line which makes the code more readable by getting error handling out of the way:

int f(x) {
   if (x<0) return ErrLog(-1, "f error, %d too small", x);
   ...
}

(instead of

int f(x) {
   if (x<0) {
      Log("f error, %d too small", x);
      return -1;
   }
   ...
}

)

The problem I have is if f returns void. I would like to do

void f(x) {
   if (x<0) return ErrLog(void, "f error, %d too small", x);
   ...
}

But this does not compile.

I thought about specialization, that is adding:

inline void ErrLog(const char* Format, ...)
{
    va_list args; va_start( args, Format ); _vsnprintf(mypWorkBuffer, MaxLogLength, Format, args); va_end(args);
    // do some fancy logging with mypWorkBuffer
    return;
}

This allows me to do

return ErrLog("f error, %d too small", x);

However I'm not sure it's safe for functions returning char*. For example consider:

char* f(x) {
   if (x<0) return ErrLog("error", "f error , %d too small", x);
   ...
}

I think this one will match both template and specialization.

Any thoughts/better solutions ?

Upvotes: 1

Views: 4206

Answers (3)

halfflat
halfflat

Reputation: 1584

I think it is more idiomatic and much, much simpler to just use the comma operator:

inline void Errlog(const char *format,...) { /* ... */ }

void f(int x) {
    if (x<0) return Errlog("f error...");
}

double g(double x) {
    if (x<0) return Errlog("x is negative..."),-1.0;
    else return sqrt(x);
}

The left hand size of the comma operator can be a void expression.

This obviates the need to use a templated function at all.

Edit: if you really don't like using comma...

You have three options if you really want to use a function interface. Given a generic function (I'm using std::forward here, so that we can use it with references and so on):

template <class T>
inline T &&ErrLog(T &&ret,const char *format,...) {
    /* logging ... */
    return std::forward<T>(ret);
}

You can either:

1) Use a separate function for the void case.

void ErrLogV(const char *format,...) {
    /* logging ... */
}

void foo1(int x) {
    if (x<0) return ErrLogV("foo1 error");
}

2) Overload with a special 'tag' type:

static struct errlog_void_t {} errlog_void;

inline void ErrLog(errlog_void_t,const char *format,...) {
    /* logging ... */
}

void foo2(int x) {
    if (x<0) return ErrLog(errlog_void,"foo2 error");
}

3) Or use a throw away argument and cast to void:

void foo3(int x) {
    // uses generic ErrLog():
    if (x<0) return (void)ErrLog(0,"foo3 error");
}

Second edit: why the version without the ret parameter can't work

You can safely define the version with ret; it's not ambiguous, but it won't necessarily be used when you want it to be. The fundamental problem is that in one context you will want one behaviour, and in another the other behaviour, but the function call parameters will have the exact same type.

Consider the following example:

#include <cstdarg>
#include <cstdio>
#include <utility>

using namespace std;

template <typename T>
T &&foo(T &&x,const char *format,...) {
    puts("T-version");
    va_list va;
    va_start(va,format);
    vprintf(format,va);
    va_end(va);
    puts("");
    return forward<T>(x);
}

void foo(const char *format,...) {
    puts("void-version");
    va_list va;
    va_start(va,format);
    vprintf(format,va);
    va_end(va);
    puts("");
}

int main() {
    foo("I want the void overload: %s, %s","some string","some other string");
    foo("I want to return this string","I want the const char * overload: %s","some string");
}

This will compile! But only the first version of foo will be called. The compiler cannot distinguish your intent from the argument types.

Why is it not ambiguous?

Both versions of foo will be candidates for overload resolution, but the first one will be a better match. You can refer to a detailed description of the process (especially the section on ranking), but in short, when you have two or more const char * arguments, the const char * second parameter of the first version of foo is more specific than the ellipsis parameter of the second version.

If you have only one const char * argument, then the void-returning version will win over the generic one, because non-template overloads are prefered to template-overloads, other things being equal:

foo("this will use the void-version");

In short, using the overload will compile, but will give surprising and hard to debug behaviour, and can't handle the case where your void-returning version takes more than one argument.

Upvotes: 6

dau_sama
dau_sama

Reputation: 4347

I suggest you to use template specialization with a user defined Void type, for handling your Void case. Sorry this is C++14, but you should be able to convert it really easily, just change the return types accordingly

template<class T>
auto ErrLog(T ret, const char* Format, ...)
{
    return ret;
}

struct Void{ };

template<>
auto ErrLog(Void ret, const char* Format, ...)
{
    return;
}

int main()
{
    ErrLog(Void{}, "f error, %d too small");
    ErrLog(-1, "f error, %d too small");
}

Also I didn't pass the variadic template Args, this solution is more to show you this idea.

Upvotes: 0

Serge Ballesta
Serge Ballesta

Reputation: 148870

For the void question, use an int specialization returning 0 and later ignore it :

void f(x) {
   if (x<0) {
      ErrLog(0, "f error, %d too small", x);
      return;
   }
   ...
}

For functions returning char * you can make it safe provided the returned value is static (but as you are using C++ you could alse use std::string) :

char* f(x) {
   static char err[] = "error";
   if (x<0) return ErrLog(err, "f error , %d too small", x);
   ...
}

EDIT :

For the void part, I cannot imagine how you can avoid a block (independantly of the templating question) :

void g(int x);
void f(int x) {
    if (x<0) return g(x); // 2 errors here : g returns void and f returns void
}

If a function g returns void, you cannot use it as the value of a return statement, whatever this function is. And anyway you cannot use a return something; in a function returning void. The best I can imagine (but it is not what you asked) is :

void f(x) {
   if (x<0) ErrLog(0, "f error, %d too small", x);
   else {
      ...
   }
}

creating another block for the else part ...

Upvotes: 0

Related Questions