psprint
psprint

Reputation: 359

Implementing optional argument of an variadic-using function?

I have a function:

log(const char *domain, int log_level, const char *fmt, ...)

I would like first and second arguments optional, so it's like following calls would be possible:

log("SYSTEM-A", 1, "Example %s", "...message");
log(1, "Example %s", "...message");
log("Example %s", "...message");

I've read about neat macro tricks, however they (almost?) all rely on trailing arguments to 'stand out' in a helper macro:

HELPER_SELECT(_1, _2, _3, func, ...) func

I however cannot use this method, because log() can take arbitrary number of variadic arguments. Is this possible to overcome somehow? With use of _Generics, maybe?

Upvotes: 1

Views: 347

Answers (2)

KamilCuk
KamilCuk

Reputation: 140880

(1) log("SYSTEM-A", 1, "Example %s", "...message");
(2) log(1, "Example %s", "...message");
(3) log("Example %s", "...message");

From what I understand:

  • (1) does not has % in it's first argument.
  • (2) first argument is int
  • (3) has % in it's argument.

You can:

  • overload log macro on number of arguments
  • if one argument
    • choose (3)
  • else
  • _Generic on first argument
  • If first argument is an int
    • choose (2)
  • Else
    • call some _log_wrapper(const char *arg, ...)
      • inspect if strchr(arg, '%')
        • if it does, call va_list version of (3)
      • if it does not, call va_list version of (1)

A possible implementation looks like this:

#include <stdarg.h>
#include <stdio.h>
#include <string.h>

void vlog_domain(const char *domain, int log_level, const char *fmt, va_list va)  {
    printf("domain\n");
}
void vlog_level(int log_level, const char *fmt, va_list va) {
    printf("level\n");
}
void vlog_normal(const char *fmt, va_list va) {
    printf("normal\n");
}

void _log_wrapper(int type, ...) {
    va_list va;
    va_start(va, type);
    if (type == 1) {
        int log_level = va_arg(va, int);
        const char *fmt = va_arg(va, const char *);
        vlog_level(log_level, fmt, va);
    } else {
        const char *arg = va_arg(va, const char*);
        if (!strchr(arg, '%')) {
            const char *domain = arg;
            int log_level = va_arg(va, int);
            const char *fmt = va_arg(va, const char*);
            vlog_domain(domain, log_level, fmt, va);
        } else {
            const char *fmt = arg;
            vlog_normal(fmt, va);
        }
    }
    va_end(va);
}

#define _log_1(_1)  vlog_normal(_1) // TODO
#define _log_2(_1, ...)  _log_wrapper( \
        _Generic((_1), int: 1, char *: 2), _1, ##__VA_ARGS__)
// this implementation supports max ca. 10 arguments
#define _log_N(_9,_8,_7,_6,_5,_4,_3,_2,_1,_0,N,...)  _log_##N
#define log(...)  _log_N(__VA_ARGS__,2,2,2,2,2,2,2,2,2,2,1)(__VA_ARGS__)

int main() {
    log("SYSTEM-A", 1, "Example %s", "...message"); // domain
    log(1, "Example %s", "...message"); // level
    log("Example %s", "...message"); // normal
}

These are some time spent on writing the interface, that the next developer will most probably anyway not understand and will have to rewrite and refactor the whole code. I suggest instead to be as possible clear and write as possibly easy code to understand and just name your functions:

 logd("SYSTEM-A", 1, "Example %s", "...message");
 logl(1, "Example %s", "...message");
 log("Example %s", "...message");

and be done with it.

Inspect other projects how they solved logging with "domain+loglevel" (which sounds like syslog() severity and facility....) have a look how other projects solved logging interface. From my mind I enjoyed zephyr project solved logging, and it's open source so see inspect it's sources.

Upvotes: 2

Oka
Oka

Reputation: 26345

This isn't really possible in C - variadic functions solve this problem once, but you're trying to solve it twice.

Consider the use of a simple options structure. Have all options empty value be equivalent to 0 (or have a default initializer), so that the caller doesn't need to remember any sentinel values.

struct loggeropts {
    const char *domain;
    int level;
};

void logger(struct loggeropts *opts, const char *fmt, ...) {
    if (opts) {
        if (opts->domain) { /* */ }
        if (opts->level) { /* */ }
    }

    /* impl */
}

int main(void) {
    struct loggeropts opts = { .domain = "SYSTEM-A" };            
                                                                                                                      
    logger(&opts, "#Example %u %s", 42, "information");                                                               
    logger(NULL, "#Example %u %s", 44, "different information");
}

If optional arguments aren't used that often, you could hide the call behind a macro.

#define logger(string, ...) logger_with_opts(NULL, string, __VA_ARGS__)
void logger_with_opts(struct loggeropts *opts, const char *fmt, ...);

Alternatively, have a special section of your format string that identifies passed options, and make sure they are passed before the usual variadic arguments. Remembering to move your fmt pointer before passing it onward. This does seem fragile, though, and has additional overhead.

logger("{{@d@l}}Example %s", "SYSTEM-A", 1, "...message");
logger("{{@d}}Example %s", "SYSTEM-B", "...message");

I would most likely suggest just simply having domain-specific logging functions, in addition to the general purpose function.

Upvotes: 0

Related Questions