Я создал функцию, которая печатает пары ключей и значений, где ключам доверяют строки литералов времени компиляции, которые, возможно, могут содержать спецификаторы printf. Теперь мой вопрос: законна ли эта функция в соответствии со стандартом c (например, c99)? Если нет, могу ли я сделать что-нибудь, чтобы соответствовать?
void pairs(char *message, ...)
{
va_list args;
va_start(args, message);
char *key = va_arg(args, char *);
if (key == NULL) {
va_end(args);
return;
}
while (key != NULL) {
if (key[0] == '%') {
int len = strlen(key);
printf("%s = ", &key[len + 1]);
// Copy the current state of the va_list and pass it to vprintf
va_list args2;
va_copy(args2, args);
vprintf(key, args2);
va_end(args2);
// Consume one argument assuming vprintf consumed one
va_arg(args, char *);
} else {
// if no format assume it is a string
fprintf(_log_stream, "%s=\"%s\"", key, va_arg(args, char *));
}
key = va_arg(args, char *);
if (key == NULL)
break;
printf(", ");
}
va_end(args);
}
Это должно работать что-то вроде
pairs("This is a beautiful value",
"%d\0hello", 10,
"pass", user_var,
"hello", "bye", NULL);
И на самом деле выводится правильно: hello=10, pass = "sus", hello = "bye"
Итак, это работает на моей машине (с использованием gcc). Но опять же, соответствует ли это требованиям?
Редактировать: Я нашел https://www.gnu.org/software/libc/manual/html_node/Variable-Arguments-Output.html
Примечание по переносимости: значение указателя va_list не определено после вызова vprintf, поэтому вы не должны использовать va_arg после вызова vprintf. Вместо этого вам следует вызвать va_end, чтобы удалить указатель из обслуживания. Вы можете снова вызвать va_start и начать выборку аргументов с начала списка переменных аргументов. (В качестве альтернативы вы можете использовать va_copy, чтобы сделать копию указателя va_list перед вызовом vfprintf.) Вызов vprintf не уничтожает список аргументов вашей функции, а только конкретный указатель, который вы ей передали.
Именно по этой причине я в первую очередь использую va_copy, но я не уверен, есть ли способ обойти это ограничение.
Нет проблем передать частично использованный va_list
и/или va_list
, содержащий лишние аргументы, в vprintf
.
Первоначальное утверждение if (key == NULL)
кажется излишним.
Если вы можете легально вызвать vprintf(key, args);
, то я не понимаю, почему вы не можете вызвать vprintf(key, args2);
. Вам интересно, можно ли вызвать vprintf()
с частично использованным va_list?
Или вас интересуют дополнительные аргументы, которые не используются строкой формата?
Логика «потребления одного аргумента» кажется неправильной, поскольку предполагает, что следующим аргументом будет char *
.
Дополнительные аргументы функции printf игнорируются. Так что второй вопрос не проблема.
На практике я думаю, что, скорее всего, будет безопасно сделать это вообще без копирования, поскольку vprintf()
будет использовать аргументы, необходимые для строки формата. К сожалению, в спецификации сказано, что состояние arg
после вызова функций vXXX
является неопределенным. Это позволит избежать проблемы, на которую указывает @IanAbbott.
@Barmar да, мне интересно, могу ли я va_copy частично использованный va_list.
@IanAbbott Да, я только что заметил. Я ввел это, потому что где-то читал, что vprintf ничего не гарантирует в отношении va_list. Знаете ли вы какой-нибудь способ правильно употреблять арг?
Вам нужно будет знать тип (расширенный аргумент по умолчанию) используемого аргумента.
Или вы можете позволить vprintf
использовать исходные аргументы, а не копию.
Проигнорируйте мой комментарий: «Или вы могли бы позволить vprintf
использовать исходные аргументы, а не копию». Как отметил @nect в комментариях к моему (удаленному) ответу, это UB. Они процитировали примечание о переносимости в руководстве GCC, но соответствующая ссылка в C17 — §7.16/3 «… Объект ap
может быть передан в качестве аргумента другой функции; если эта функция вызывает макрос va_arg
с параметром ap
, значение ap
в вызывающей функции является неопределенным и должен быть передан макросу va_end
до любой дальнейшей ссылки на ap
."
Если бы существовала альтернатива vprintf
, которая принимала указатель на va_list
, это работало бы, как разрешено сноской 257 C17: «Разрешается создать указатель на va_list
и передать этот указатель другой функции, и в этом случае исходный функция может продолжать использовать исходный список после возврата из другой функции.». К сожалению, такой альтернативной функции, определенной стандартом, не существует.
Это использование неопределенного поведения va_copy?
Нет.
легальна ли эта функция в соответствии со стандартом c (например, c99)?
Нет.
key = va_arg(args, char *);
это неопределенное поведение. Следующий аргумент — 10
, то есть int
.
(Кроме того, технически NULL
не является char *
. NULL
является либо int
, либо void *
, никто не знает. Это также недопустимо для NULL
. Но в настоящее время NULL
является универсальным (void *)0
, и в нормальных системах все указатели имеют одинаковый размер, Итог, если вы используете char *
, никто не заметит. Но если вы хотите быть правым, аргумент должен быть (char *)NULL
.)
Если нет, могу ли я сделать что-нибудь, чтобы соответствовать?
То, что вы хотите сделать так, как вы хотите, невозможно. Вы не сможете использовать va_list
после того, как vprintf
его использовал. После того, как vprintf
использовал va_list
, вам необходимо va_end
это сделать.
Если вы хотите это сделать, вам придется самостоятельно проанализировать строку формата и самостоятельно получить типы аргументов, а также заранее va_list
самостоятельно реализовать vprintf
и либо реализовать pairs
самостоятельно, либо вызвать его отдельно.
могу ли я что-нибудь сделать
О да, конечно, полностью. Итак, давайте реализуем printf
как переменный макрос, который просто перенаправляет printf
.
#include <boost/preprocessor/facilities/overload.hpp>
#include <stdio.h>
#define PAIRS1_0()
#define PAIRS1_2(a, b) " " a
#define PAIRS1_4(a, b, ...) " " a PAIRS1_2(__VA_ARGS__)
#define PAIRS1_6(a, b, ...) " " a PAIRS1_4(__VA_ARGS__)
#define PAIRS2_0()
#define PAIRS2_2(a, b) b
#define PAIRS2_4(a, b, ...) b, PAIRS2_2(__VA_ARGS__)
#define PAIRS2_6(a, b, ...) b, PAIRS2_4(__VA_ARGS__)
#define pairs(pre, ...) \
printf( \
pre \
BOOST_PP_OVERLOAD(PAIRS1_, __VA_ARGS__)(__VA_ARGS__) \
"\n", \
BOOST_PP_OVERLOAD(PAIRS2_, __VA_ARGS__)(__VA_ARGS__) \
)
int main() {
pairs("This is a beautiful value",
"hello=%d", 10,
"hello=%s", "bye");
}
Вызов становится одиночным printf
с аргументами, соединенными пробелами. Первый перегруженный макрос объединяет строки в одну строку, а затем второй перегруженный макрос принимает вторые аргументы. Чтобы изменить порядок аргументов для _Generic
, достаточно просто перетасовать.
Но это неудовлетворительно. Вы можете углубиться в это подробнее, используя va_list
, чтобы угадать спецификатор формата. Из аргументов вы можете создать массив «принтеров», которые принимают va_list
по указателю. Не по стоимости! Изменить «верхнюю» функцию "hello"
можно только передав ей указатель. Затем вы можете перебирать принтеры, чтобы печатать значения одно за другим.
#include <stdio.h>
#include <stdarg.h>
// printers for pairs
void pairs_int(va_list *va) {
const int v = va_arg(*va, int);
printf("%d", v);
}
void pairs_charp(va_list *va) {
char *p = va_arg(*va, char *);
printf("%s", p);
}
#define PRINTER(x) \
_Generic((x) \
, int: &pairs_int \
, char *: &pairs_charp \
)
typedef void (*printer_t)(va_list *va);
// the actual logic
void pairs_in(const char *pre, const printer_t printers[], ...) {
va_list va;
va_start(va, printers);
fputs(pre, stdout);
for (const printer_t *printer = printers; printer; printer++) {
putchar(' ');
fputs(va_arg(va, char*), stdout);
putchar('=');
(*printer)(&va);
fflush(stdout);
}
va_end(va);
putchar('\n');
}
// Convert arguments into an array of printers
#define PRINTERS_0()
#define PRINTERS_2(a, b) PRINTER(b)
#define PRINTERS_4(a, b, ...) PRINTERS_2(a, b), PRINTERS_2(__VA_ARGS__)
#define PRINTERS_6(a, b, ...) PRINTERS_2(a, b), PRINTERS_4(__VA_ARGS__)
#define PRINTERS_N(_6,_5,_4,_3,_2,_1,N,...) PRINTERS##N
#define PRINTERS(...) PRINTERS_N(__VA_ARGS__,_6,_5,_4,_3,_2,_1)(__VA_ARGS__)
// The entrypoint
#define pairs(pre, ...) \
pairs_in(pre, (const printer_t[]){ \
PRINTERS(__VA_ARGS__), NULL, }\
__VA_OPT__(,) \
__VA_ARGS__ \
)
int main() {
pairs("This is a beautiful value",
"value", 10,
"hello", "bye");
}
Теперь вы можете расширить этот метод, например, включив в "%d\0hello"
спецификатор формата, как вы хотели, а затем извлечь его. Например, %
, если строка начинается с \0
, разделите ее на 10
и используйте ведущую часть для печати внутри принтера. Я думаю, что это тот интерфейс, к которому вы стремились.
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// printers for pairs
const char *_extract_fmt(const char *text, const char *def) {
return text[0] == '%' ? text : def;
}
const char *_extract_name(const char *text) {
return text[0] == '%' ? text + strlen(text) + 1 : text;
}
void pairs_int(const char *text, va_list *va) {
const int v = va_arg(*va, int);
printf(_extract_fmt(text, "%d"), v);
}
void pairs_charp(const char *text, va_list *va) {
char *v = va_arg(*va, char *);
printf(_extract_fmt(text, "%s"), v);
}
#define PRINTER(x) _Generic((x), int: &pairs_int, char *: &pairs_charp)
typedef void (*printer_t)(const char *text, va_list *va);
// the actual logic
void pairs_in(const char *pre, const printer_t printers[], ...) {
va_list va;
va_start(va, printers);
fputs(pre, stdout);
for (const printer_t *printer = printers; *printer; printer++) {
putchar(' ');
char *text = va_arg(va, char *);
fputs(_extract_name(text), stdout);
putchar('=');
(*printer)(text, &va);
fflush(stdout);
}
va_end(va);
putchar('\n');
}
// Convert arguments into an array of printers
#define PRINTERS_0()
#define PRINTERS_2(a, b) PRINTER(b)
#define PRINTERS_4(a, b, ...) PRINTERS_2(a, b), PRINTERS_2(__VA_ARGS__)
#define PRINTERS_6(a, b, ...) PRINTERS_2(a, b), PRINTERS_4(__VA_ARGS__)
#define PRINTERS_N(_6, _5, _4, _3, _2, _1, N, ...) PRINTERS##N
#define PRINTERS(...) PRINTERS_N(__VA_ARGS__, _6, _5, _4, _3, _2, _1)(__VA_ARGS__)
// The entrypoint
#define pairs(pre, ...) \
pairs_in(pre, (const printer_t[]){ \
PRINTERS(__VA_ARGS__), \
NULL, \
} __VA_OPT__(, ) __VA_ARGS__)
int main() {
pairs("This is a beautiful value", "%10d\0value", 10, "hello", "bye");
}
Или вы можете расширить такую библиотеку и начать поддерживать строки формата Python и так далее, что я и сделал с уже мертвой библиотекой yio, так как у меня нет на это времени.
спасибо, я попробую заставить макросы работать с c99 :p
Я думаю, это должно сработать. Я думаю, вы также можете включить препроцессор Boost в C99. Я не знаю, как использовать BOOST_PP_FOREACH для двух аргументов одновременно, чтобы это упростить, поэтому я использовал BOOST_PP_OVERLOAD и написал перегрузки. Если вам не нужен буст, то рядом #define PRINTERS_N
показано, как перегрузить макрос самостоятельно. Я могу отредактировать его, если хотите.
Это использование неопределенного поведения va_copy?
va_copy
сам по себе не является неопределенным и не передает копию vprintf
. Также не следует использовать va_end
после возврата функции — это обязательно.
Но это проблема:
// Consume one argument assuming vprintf consumed one va_arg(args, char *);
Вообще говоря, вы не можете использовать переменный аргумент, не зная его (повышенного) типа или достаточно близко к нему. В этом отношении совместимые типы взаимозаменяемы. Соответствующие целочисленные типы со знаком и без знака здесь взаимозаменяемы при условии, что фактическое значение представимо в обоих. Кроме того, типы char *
и void *
здесь взаимозаменяемы. Если тип, указанный для va_arg
, не входит в число этих альтернатив или если вообще больше нет переменных аргументов, то UB является результатом вызова va_arg
.
Некоторые из ваших альтернатив:
pairs
как функциональный макрос вместо настоящей функции (с использованием большой дозы макромагии), или
При вызове функции с переменным числом аргументов всегда следует приводить
NULL
к типу указателя:(char *)NULL
.