实现可变参数使用函数的可选参数?

Implementing optional argument of an variadic-using function?

我有一个功能:

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

我希望第一个和第二个参数是可选的,因此可以进行以下调用:

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

我读过一些简洁的宏技巧,但是它们(几乎?)都依赖于辅助宏中 'stand out' 的尾随参数:

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

但是我不能使用这个方法,因为 log() 可以接受任意数量的可变参数。这有可能以某种方式克服吗?使用 _Generics,也许?

这在 C 中实际上是不可能的 - 可变参数函数一次解决了这个问题,但你试图解决它两次。

考虑使用简单的选项结构。让所有选项 空值 等同于 0 (或具有默认初始化程序),以便调用者不需要记住任何标记值。

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");
}

如果不经常使用可选参数,您可以将调用隐藏在宏后面。

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

或者,在格式字符串中有一个特殊部分来标识传递的选项,并确保它们在通常的可变参数之前传递。记住在向前传递之前移动你的 fmt 指针。不过,这看起来确实很脆弱,并且有额外的开销。

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

除了通用功能之外,我很可能会建议仅具有特定于域的日志记录功能。

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

据我了解:

  • (1) 的第一个参数中没有 %
  • (2) 第一个参数是 int
  • (3) 的参数中有 %

您可以:

  • 根据参数数量重载 log
  • 如果一个参数
    • 选择 (3)
  • 其他
  • _第一个参数是通用的
  • 如果第一个参数是一个整数
    • 选择 (2)
  • 其他
    • 打电话给一些 _log_wrapper(const char *arg, ...)
      • 检查是否 strchr(arg, '%')
        • 如果是,请调用 (3) 的 va_list 版本
      • 如果没有,调用 (1)va_list 版本

可能的实现如下所示:

#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
}

这些是花在写界面上的一些时间,下一个开发者很可能无论如何都看不懂,将不得不重写和重构整个代码。相反,我建议尽可能清楚并编写尽可能容易理解的代码,并仅命名您的函数:

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

并完成它。

检查其他项目如何使用“domain+loglevel”(这听起来像 syslog() severity and facility....)解决日志记录问题,看看其他项目如何解决日志记录接口问题。在我看来,我喜欢 zephyr project solved logging,而且它是开源的,所以请查看它的来源。