确保只将指针传递给可变参数函数

时间:2017-11-06 13:12:07

标签: c function pointers preprocessor variadic

我在自定义CPU上构建了一个类似printf的可变参数函数,用于记录。

为了确保我没有内存错误,我编写了函数,以便它只需要指针并处理它们。预处理器总是在va-list的末尾添加一个NULL-Pointer,所以每当函数重新定位一个零指针时它就会返回。通过这种方式,我可以确定我没有阅读列表。 我这样做是为了防止错误处理程序,它应该是一个安全的功能。用户放入va-list的任何内容都不应以崩溃告终。

我现在唯一的问题是确保用户只能传递指向可变参数函数的指针,除了前两个不属于va-list的参数之外,我的函数声明如下所示:< / p>

void LogNew( UINT LogClass, char* pMsg, ... );

null-Termination:

#define LogNew(...) LogNew(__VA_ARGS__, 0)

是否有办法(可能通过预处理器)确保没有人使用按值调用而不是按引用调用。

提前谢谢你!

2 个答案:

答案 0 :(得分:0)

您可以将每个参数包装在一个表达式中,以确保只允许指针(同时保留其值)。例如,您可以将参数elem转换为:

(true ? (elem) : (void *) 0)

三元条件确保其第二个和第三个参数兼容。请注意,这仍然允许将零作为参数;你可以在运行时检测到它(相应的参数将是一个空指针);此外,对于许多实现,这只会产生警告而不是硬错误。

另一种选择是将&*运算符应用于参数。这里的问题是,这对于void指针不起作用,但_Generic selection macro可以提供帮助:

&*(_Generic((elem), void *: ((char*) elem), const void *: ((char const*) elem), default: (elem)))

包装每个参数是a bit tricky,但是有一些宏编程库可以提供帮助。例如,使用Boost.Preprocessor可以写:

#include <boost/preprocessor.hpp>

void LogNew(unsigned LogClass, char* pMsg, ... ) {}
#define LOG_NEW_ELEMENT(r,data,elem) \
    , &*(_Generic((elem), void *: ((char*) elem), const void *: ((char const*) elem), default: (elem)))
#define LOG_NEW_IMPL(LogClass,Args) \
    LogNew(LogClass, \
    BOOST_PP_SEQ_HEAD(Args) \
    BOOST_PP_SEQ_FOR_EACH(LOG_NEW_ELEMENT, _, BOOST_PP_SEQ_TAIL(Args)), \
    ((void*) 0))
#define LogNew(LogClass,...) \
    LOG_NEW_IMPL(LogClass, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__))

Example

int main() {
    char msg[] = "hello";
    LogNew(1u, msg);
    LogNew(1u, msg, "world");
    LogNew(1u, msg, (void *) "ok");
    LogNew(1u, msg, 123);  // error: invalid type argument of unary '*' (have 'int')
}

答案 1 :(得分:0)

如果您希望减少或防止错误格式的错误,我认为您最好使用fprintf。它是一个可变函数,因此不安全,但它是 在C中打印东西的方式,所以程序员熟悉它及其缺点。如果您的日志记录功能只是一个宏,例如:

enum {Fatal, Error, Warning, Info};

int maxlevel = Error;

#define logmsg(L, ...) if (L > maxlevel); else {        \
        if (L == Fatal) fprintf(stderr, "Fatal: ");     \
        if (L == Error) fprintf(stderr, "Error: ");     \
                                                        \
        fprintf(stderr, __VA_ARGS__);                   \
        fprintf(stderr, "\n");                          \
                                                        \
        if (L == Fatal) exit(1);                        \
    }                                                   \

您将获得静态分析的好处,它会警告您格式/类型不匹配。在clang / gcc中,-Wformat-Wall的一部分,会做到这一点。在Visual Studio中,您可以使用/analyze。 (并且在禁止记录时不会生成va列表。)

如果您推送自己的变量参数函数并调用vfprintf,则可以detect bad format strings使用clang / gcc format属性或Visual Studio中的_Printf_format_string_ anotation。

也就是说,如果所有变量参数都是相同的类型,则可以使用数组。如果您的编译器支持C99的compound literals,您可以使用宏将参数列表的变量部分转换为数组。

假设所有打印的参数都应为const char *。 (我知道这不是你想要的,但请耐心等待。)然后你可以实现一个打印功能,可以接受任何正数量的C字符串,如下所示:

void put(const char **s)
{
    while (*s) {
        fputs(*s++, stdout);
    }

    fputs("\n", stdout);
}

#define put(...) put((const char *[]){__VA_ARGS__, NULL})

并像这样使用它:

put("Couldn't open \"", fn, "\".");
put("My name is ", (rand() % 2) ? "Igor" : "Tamara", ".");
put("");

您可以extend this techniqueconst void *。我已将代码隐藏在Ideone链接后面,因为使用void会使您丢失参数的类型信息。您的代码可能不会崩溃,因为指针应该指向某处,但是当您使用错误的格式时,仍然会调用未定义的行为并获得垃圾输出。

(在对第一个答案的评论中,Lundin指出将整数类型转换为指针是合法的。这不是所示答案的缺陷,这是该语言的缺陷。您也可以说puts(54)并且只会收到警告。)