我在自定义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)
是否有办法(可能通过预处理器)确保没有人使用按值调用而不是按引用调用。
提前谢谢你!
答案 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__))
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 technique到const void *
。我已将代码隐藏在Ideone链接后面,因为使用void
会使您丢失参数的类型信息。您的代码可能不会崩溃,因为指针应该指向某处,但是当您使用错误的格式时,仍然会调用未定义的行为并获得垃圾输出。
(在对第一个答案的评论中,Lundin指出将整数类型转换为指针是合法的。这不是所示答案的缺陷,这是该语言的缺陷。您也可以说puts(54)
并且只会收到警告。)