Это использование неопределенного поведения va_copy?

Я создал функцию, которая печатает пары ключей и значений, где ключам доверяют строки литералов времени компиляции, которые, возможно, могут содержать спецификаторы 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, но я не уверен, есть ли способ обойти это ограничение.

При вызове функции с переменным числом аргументов всегда следует приводить NULL к типу указателя: (char *)NULL.

Barmar 19.03.2024 18:45

Нет проблем передать частично использованный va_list и/или va_list, содержащий лишние аргументы, в vprintf.

Ian Abbott 19.03.2024 18:48

Первоначальное утверждение if (key == NULL) кажется излишним.

Ian Abbott 19.03.2024 18:48

Если вы можете легально вызвать vprintf(key, args);, то я не понимаю, почему вы не можете вызвать vprintf(key, args2);. Вам интересно, можно ли вызвать vprintf() с частично использованным va_list?

Barmar 19.03.2024 18:51

Или вас интересуют дополнительные аргументы, которые не используются строкой формата?

Barmar 19.03.2024 18:53

Логика «потребления одного аргумента» кажется неправильной, поскольку предполагает, что следующим аргументом будет char *.

Ian Abbott 19.03.2024 18:54

Дополнительные аргументы функции printf игнорируются. Так что второй вопрос не проблема.

Barmar 19.03.2024 18:54

На практике я думаю, что, скорее всего, будет безопасно сделать это вообще без копирования, поскольку vprintf() будет использовать аргументы, необходимые для строки формата. К сожалению, в спецификации сказано, что состояние arg после вызова функций vXXX является неопределенным. Это позволит избежать проблемы, на которую указывает @IanAbbott.

Barmar 19.03.2024 19:02

@Barmar да, мне интересно, могу ли я va_copy частично использованный va_list.

nect 19.03.2024 19:02

@IanAbbott Да, я только что заметил. Я ввел это, потому что где-то читал, что vprintf ничего не гарантирует в отношении va_list. Знаете ли вы какой-нибудь способ правильно употреблять арг?

nect 19.03.2024 19:03

Вам нужно будет знать тип (расширенный аргумент по умолчанию) используемого аргумента.

Ian Abbott 19.03.2024 19:12

Или вы можете позволить vprintf использовать исходные аргументы, а не копию.

Ian Abbott 19.03.2024 19:14

Проигнорируйте мой комментарий: «Или вы могли бы позволить vprintf использовать исходные аргументы, а не копию». Как отметил @nect в комментариях к моему (удаленному) ответу, это UB. Они процитировали примечание о переносимости в руководстве GCC, но соответствующая ссылка в C17 — §7.16/3 «… Объект ap может быть передан в качестве аргумента другой функции; если эта функция вызывает макрос va_arg с параметром ap, значение ap в вызывающей функции является неопределенным и должен быть передан макросу va_end до любой дальнейшей ссылки на ap."

Ian Abbott 19.03.2024 19:50

Если бы существовала альтернатива vprintf, которая принимала указатель на va_list, это работало бы, как разрешено сноской 257 C17: «Разрешается создать указатель на va_list и передать этот указатель другой функции, и в этом случае исходный функция может продолжать использовать исходный список после возврата из другой функции.». К сожалению, такой альтернативной функции, определенной стандартом, не существует.

Ian Abbott 19.03.2024 19:56
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
14
93
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Это использование неопределенного поведения 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

nect 20.03.2024 00:46

Я думаю, это должно сработать. Я думаю, вы также можете включить препроцессор Boost в C99. Я не знаю, как использовать BOOST_PP_FOREACH для двух аргументов одновременно, чтобы это упростить, поэтому я использовал BOOST_PP_OVERLOAD и написал перегрузки. Если вам не нужен буст, то рядом #define PRINTERS_N показано, как перегрузить макрос самостоятельно. Я могу отредактировать его, если хотите.

KamilCuk 20.03.2024 09:22

Это использование неопределенного поведения 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 как функциональный макрос вместо настоящей функции (с использованием большой дозы макромагии), или
  • переосмыслите функцию, чтобы получать больше или по-другому организованную информацию, которая лучше соответствует вашим целям.

Другие вопросы по теме