【问题标题】:Implementing optional argument of an variadic-using function?实现可变参数使用函数的可选参数?
【发布时间】:2021-04-22 21:46:50
【问题描述】:

我有一个函数:

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

我读过关于简洁的宏技巧,但是它们(几乎?)都依赖尾随参数来在辅助宏中“脱颖而出”:

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

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

【问题讨论】:

  • 当这些参数丢失时函数会做什么?你能不能只拥有用默认值填充缺失参数的宏。例如LOG_NO_DOMAIN(level, fmt, ...) log(NULL, level, fmt, __VA_ARGS__)?
  • 不要使用log作为函数名,因为它是一个标准的C函数取自然对数。
  • 函数如何知道第二个提供的参数是否“可选”?它不能。不在 C 中。
  • 有点可行。然而,案例 1 和 3 缺乏独特性,因为两者都以 const char * 开头,然后是 int 或 也许 是 int。想想log("Ex %d %s", 42, "message"); 看起来像log(const char *domain, int log_level, const char *fmt)。总的来说,我认为 OP 的目标是尽可能的,但是作为代码需要解析%specifiers 的第一个参数需要做很多工作。推荐替代目标。
  • 你如何区分 log("SYSTEM-A", 1, "Example") 和 log("Example %d%s", 1, "...message") ?

标签: c variadic-functions variadic


【解决方案1】:
(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)
  • 其他
  • _Generic 在第一个参数上
  • 如果第一个参数是 int
    • 选择 (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");

并完成它。

检查其他项目他们如何使用“域+日志级别”解决日志记录(听起来像syslog() 严重性和设施......)看看其他项目如何解决日志记录接口。在我看来,我喜欢 zephyr project solved logging,它是开源的,所以请查看它的源代码。

【讨论】:

  • 感谢您提供如此有趣且富有创意的解决方案。我已经实现了它并且它正在工作:) 我只是改变了,只有 wrapper 函数并在只给出单个参数时传递不同的类型(log_normal 代码中的案例)。现在我正在考虑log 函数的更独特的语法,使用非常好的_Generic - 通常,_Generic 是一个臃肿的大代码,但它不是:) 所以,首先要传递什么争论?一些用户数据,或者可能——来自 GLib 的 GError?很酷,然后我可以以标准方式附加它的消息 :) :)
【解决方案2】:

这在 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");

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

【讨论】:

    猜你喜欢
    • 2014-07-11
    • 1970-01-01
    • 2021-01-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-12
    相关资源
    最近更新 更多