1

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?

6
  • What would the function do when those args are missing? Can you not just have macros that fill the missing args with default values. e.g LOG_NO_DOMAIN(level, fmt, ...) log(NULL, level, fmt, __VA_ARGS__)? Commented Apr 22, 2021 at 21:50
  • Do not use log as a function name as it is a standard C function for taking natural logarithm. Commented Apr 22, 2021 at 21:50
  • How can the function know whether or not the second provided argument is "optional" or not? It can't. Not in C. Commented Apr 22, 2021 at 21:52
  • Somewhat doable. Yet case 1 and 3 lack distinctiveness as both start with const char * then an int or maybe an int. Consider how log("Ex %d %s", 42, "message"); looks like the log(const char *domain, int log_level, const char *fmt). Overall I see OP's goal as possible, but a lot of work as code needs to parse the first parameter for %specifiers. Recommend alternative goals. Commented Apr 22, 2021 at 22:50
  • how would you differentiate log("SYSTEM-A", 1, "Example") from log("Example %d%s", 1, "...message") ? Commented Apr 23, 2021 at 8:24

2 Answers 2

2
(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.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for such interesting and creative solution. I've implemented it and it is working :) I've only changed so that there is only wrapper function and pass a different type when a single argument is only given (log_normal case in your code). And now I'm thinking about even more distinct syntax for the log function, having so nice _Generic use – typically, _Generic is a bloat, large code but here it isn't :) So, what to pass for first argument? Some user data, or maybe – a GError from GLib? That's cool, I could then append its message in a standard way :) :)
0

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.