Проверьте размер аргумента макроса во время компиляции - PullRequest
4 голосов
/ 22 сентября 2019

Давайте предположим, что у меня есть макрос (более подробно о почему , ниже в разделе PS)

void my_macro_impl(uint32_t arg0, uint32_t arg1, uint32_t arg2);

...

#define MY_MACRO(arg0, arg1, arg2)       my_macro_impl((uint32_t)(arg0), (uint32_t)(arg1), (uint32_t)(arg2))

HW, на котором будет использоваться этот макрос, малоendian и использует 32-битную архитектуру, поэтому все указатели имеют ширину до 32 бит (включая ее).Моя цель - предупредить пользователя, когда он ошибочно передает параметр uint64_t или int64_t.

Я думал об использовании sizeof, как это

#define MY_MACRO(arg0, arg1, arg2)       do \
                                         {  \
                                             static_assert(sizeof(arg0) <= sizeof(uint32_t));  \
                                             static_assert(sizeof(arg1) <= sizeof(uint32_t));  \
                                             static_assert(sizeof(arg2) <= sizeof(uint32_t));  \
                                             my_macro_impl((uint32_t)(arg0), (uint32_t)(arg1), (uint32_t)(arg2));  \
                                         } while (0)

Но пользователь можетиспользуйте MY_MACRO с битовым полем, а затем мой код не скомпилируется:

ошибка: неверное применение 'sizeof' к битовому полю

Вопрос : есть ли возможность обнаружить во время компиляции, если размер аргумента макроса больше, скажем, uint32_t?


PS

The MY_MACRO будет действовать аналогично printf во встроенной среде реального времени.Эта среда имеет регистратор HW, который может принимать до 5 параметров, каждый параметр должен быть 32-битным.Цель состоит в том, чтобы сохранить стандартный формат для printf.Строки формата анализируются в автономном режиме, и анализатор хорошо знает, что каждый параметр является 32-битным, поэтому он будет преобразовывать его на основе %... из строки формата.Возможные варианты использования приведены ниже.

Не желательно использовать:

uint64_t time = systime_get();
MY_MACRO_2("Starting execution at systime %llx", time); // WRONG! only the low 32 bits are printed. I want to detect it and fail the compilation.

Ожидаемое использование:

uint64_t time = systime_get();
MY_MACRO_3("Starting execution at systime %x%x", (uint32_t)(time >> 32), (uint32_t)time); // OK! 

Ответы [ 3 ]

5 голосов
/ 22 сентября 2019

Следующий подход может работать для этой потребности:

#define CHECK_ARG(arg)                  _Generic((arg), \
                                                 int64_t  : (arg),  \
                                                 uint64_t : (arg),  \
                                                 default  : (uint32_t)(arg))

Тогда MY_MACRO может быть определено как

#define MY_MACRO(a0, a1, a2)       do \
                                   {  \
                                       uint32_t arg1 = CHECK_ARG(a0);  \
                                       uint32_t arg2 = CHECK_ARG(a1);  \
                                       uint32_t arg3 = CHECK_ARG(a2);  \
                                       my_macro_impl(arg1, arg2, arg3);\
                                   } while (0)

В таком случае при передаче, например, uint64_t выдается предупреждение:

предупреждение: неявное преобразование теряет целочисленную точность: «uint64_t» (иначе «unsigned long long») в «uint32_t» (также «unsigned int») [-Wshorten-64-в-32]

Примечание:

Другие типы, такие как double, 128/256 битовые типы, могут обрабатываться аналогично.

Должны быть включены соответствующие предупреждения.

РЕДАКТИРОВАТЬ:

Вдохновленный комментариями Лундина и ответом , предложенное выше решение может быть легко изменено напортативная версия, которая вызовет ошибку компиляции, а не просто предупреждение компилятора.

#define CHECK_ARG(arg)          _Generic((arg),         \
                                         int64_t  : 0,  \
                                         uint64_t : 0,  \
                                         default  : 1)

Таким образом, MY_MACRO можно изменить на

#define MY_MACRO(a0, a1, a2)       do \
                                   {  \
                                       _Static_assert(CHECK_ARG(a1) && \
                                                      CHECK_ARG(a2) && \
                                                      CHECK_ARG(a3),   \
                                                      "64 bit parameters are not supported!"); \
                                       my_macro_impl((uint32_t)(a1), (uint32_t)(a2), (uint32_t)(a3)); \
                                   } while (0)

На этот раз при передаче uint64_tпараметр MY_MACRO(1ULL, 0, -1), компиляция завершается с ошибкой ошибка :

ошибка: static_assert не выполнен из-за требования '_Generic ((1ULL), long long: 0, без знака long long: 0, по умолчанию: 1) && (_Generic ((0), long long: 0, unsigned long long: 0, по умолчанию: 1) && _Generic ((- 1), long long: 0, unsigned long long: 0, по умолчанию: 1)) ' "64-битные параметры не поддерживаются!"

1 голос
/ 23 сентября 2019

Вопрос: есть ли возможность обнаружить во время компиляции, если размер макро-аргумента больше, скажем, uint32_t?

Единственный способ сделать это переносимым,генерирует ошибку компилятора с _Generic.Если вы хотите, чтобы ошибка была красивой и читабельной, вы передаете результат от _Generic до _Static_assert, чтобы вы могли напечатать пользовательскую строку в качестве сообщения компилятора.

Ваша спецификация выглядит так:

  • Все должно быть проверено во время компиляции.
  • Макрос может получить от 1 до 5 параметров любого типа.
  • Только int32_t и uint32_tразрешенные типы.

Это означает, что вы должны написать макрос с переменным числом аргументов, и он должен принимать от 1 до 5 параметров.


Такой макрос можно записать так:

#define COUNT_ARGS(...) ( sizeof((uint32_t[]){__VA_ARGS__}) / sizeof(uint32_t) )

#define MY_MACRO(...)                                                           \
  _Static_assert(COUNT_ARGS(__VA_ARGS__)>0 && COUNT_ARGS(__VA_ARGS__)<=5,       \
                 "MY_MACRO: Wrong number of arguments");                        

COUNT_ARGS создает временный составной литерал из столько объектов, сколько вы даете макросу.Если они полностью несовместимы с uint32_t, вы можете получить здесь ошибки / предупреждения компилятора уже здесь.Если нет, то COUNT_ARGS вернет количество переданных аргументов.


С этим мы можем выполнить фактическую, переносимую проверку типа каждого элемента в списке переменных аргументов.Чтобы проверить тип одного элемента с помощью _Generic:

#define CHECK(arg) _Generic((arg), uint32_t: 1, int32_t: 1, default: 0)

Затем передайте результат этого на _Static_assert.Однако для 5 аргументов нам необходимо проверить от 1 до 5 пунктов.Для этой цели мы можем «связать» несколько макросов:

#define CHECK(arg) _Generic((arg), uint32_t: 1, int32_t: 1, default: 0)

#define CHECK_ARGS1(arg1,...) CHECK(arg1)
#define CHECK_ARGS2(arg2,...) (CHECK(arg2) && CHECK_ARGS1(__VA_ARGS__,0))
#define CHECK_ARGS3(arg3,...) (CHECK(arg3) && CHECK_ARGS2(__VA_ARGS__,0))
#define CHECK_ARGS4(arg4,...) (CHECK(arg4) && CHECK_ARGS3(__VA_ARGS__,0))
#define CHECK_ARGS5(arg5,...) (CHECK(arg5) && CHECK_ARGS4(__VA_ARGS__,0))

Каждый макрос проверяет первый переданный ему аргумент, а затем перенаправляет остальные, если таковые имеются, следующему макросу.Конечный 0 предназначен для закрытия предупреждений ISO C об аргументах rest, необходимых для вариационных макросов.

Мы можем объединить эти вызовы в _Static_assert, который вызывает соответствующий макрос в «цепочке», соответствующейчисло аргументов:

_Static_assert(COUNT_ARGS(__VA_ARGS__) == 1 ? CHECK_ARGS1(__VA_ARGS__,0) :    \
               COUNT_ARGS(__VA_ARGS__) == 2 ? CHECK_ARGS2(__VA_ARGS__,0) :    \
               COUNT_ARGS(__VA_ARGS__) == 3 ? CHECK_ARGS3(__VA_ARGS__,0) :    \
               COUNT_ARGS(__VA_ARGS__) == 4 ? CHECK_ARGS4(__VA_ARGS__,0) :    \
               COUNT_ARGS(__VA_ARGS__) == 5 ? CHECK_ARGS5(__VA_ARGS__,0) : 0, \
               "MY_MACRO: incorrect type in parameter list " #__VA_ARGS__);   \

Полный код с примерами использования:

#include <stdint.h>

#define COUNT_ARGS(...) ( sizeof((uint32_t[]){__VA_ARGS__}) / sizeof(uint32_t) )

#define CHECK(arg) _Generic((arg), uint32_t: 1, int32_t: 1, default: 0)

#define CHECK_ARGS1(arg1,...) CHECK(arg1)
#define CHECK_ARGS2(arg2,...) (CHECK(arg2) && CHECK_ARGS1(__VA_ARGS__,0))
#define CHECK_ARGS3(arg3,...) (CHECK(arg3) && CHECK_ARGS2(__VA_ARGS__,0))
#define CHECK_ARGS4(arg4,...) (CHECK(arg4) && CHECK_ARGS3(__VA_ARGS__,0))
#define CHECK_ARGS5(arg5,...) (CHECK(arg5) && CHECK_ARGS4(__VA_ARGS__,0))

#define MY_MACRO(...)                                                           \
do {                                                                            \
  _Static_assert(COUNT_ARGS(__VA_ARGS__)>0 && COUNT_ARGS(__VA_ARGS__)<=5,       \
                 "MY_MACRO: Wrong number of arguments");                        \
  _Static_assert(COUNT_ARGS(__VA_ARGS__) == 1 ? CHECK_ARGS1(__VA_ARGS__,0) :    \
                 COUNT_ARGS(__VA_ARGS__) == 2 ? CHECK_ARGS2(__VA_ARGS__,0) :    \
                 COUNT_ARGS(__VA_ARGS__) == 3 ? CHECK_ARGS3(__VA_ARGS__,0) :    \
                 COUNT_ARGS(__VA_ARGS__) == 4 ? CHECK_ARGS4(__VA_ARGS__,0) :    \
                 COUNT_ARGS(__VA_ARGS__) == 5 ? CHECK_ARGS5(__VA_ARGS__,0) : 0, \
                 "MY_MACRO: incorrect type in parameter list " #__VA_ARGS__);   \
} while(0)


int main (void)
{
//MY_MACRO();                          // won't compile, "empty initializer braces"
//MY_MACRO(1,2,3,4,5,6);               // static assert "MY_MACRO: Wrong number of arguments"
  MY_MACRO(1);                         // OK, all parameters int32_t or uint32_t
  MY_MACRO(1,2,3,4,5);                 // OK, -"-
  MY_MACRO(1,(uint32_t)2,3,4,5);       // OK, -"-
//MY_MACRO(1,(uint64_t)2,3,4,5);       // static assert "MY_MACRO: incorrect type..."
//MY_MACRO(1,(uint8_t)2,3,4,5);        // static assert "MY_MACRO: incorrect type..."
}

Это должно быть на 100% переносимо и не зависит от компилятора, дающего дополнительную диагностику за пределамичто требуется стандартом.

Старый трюк do-while(0) предназначен для обеспечения совместимости со стандартами форматирования фигурных скобок, такими как if(x) MY_MACRO(1) else.См. Зачем использовать явно бессмысленные операторы do-while и if-else в макросах?

1 голос
/ 22 сентября 2019

Тип троичного выражения ?: является общим типом его второго и третьего аргументов (с целочисленным продвижением меньших типов).Поэтому следующая версия вашего MY_MACRO будет работать в 32-разрядной архитектуре:

static_assert(sizeof(uint32_t) == sizeof 0, ""); // sanity check, for your machine

#define MY_MACRO(arg0, arg1, arg2) \
    do {  \
        static_assert(sizeof(0 ? 0 : (arg0)) == sizeof 0, "");  \
        static_assert(sizeof(0 ? 0 : (arg1)) == sizeof 0, "");  \
        static_assert(sizeof(0 ? 0 : (arg2)) == sizeof 0, "");  \
        my_macro_impl((uint32_t)(arg0), (uint32_t)(arg1), (uint32_t)(arg2));  \
    } while (0)

Более того, это решение должно работать со всеми версиями C и C ++ (с, если, еслиобязательно, подходящее определение static_assert).

Обратите внимание, что этот макрос, как и оригинал OP, имеет семантику функции в том смысле, что аргументы оцениваются только один раз, в отличие, например, от пресловутого MAX макрос.

...