Ограничения синтаксиса имени переменной функции `##`
Иногда синтаксис C/C++ поражает меня, и сегодня один из таких дней. Я наткнулся на некоторый код, который использует макросы с ##
генерировать уникальные и индивидуальные методы доступа к конкретным аппаратным регистрам (показано ниже).
Вопрос: Какие ограничения или проблемы существуют с ##
синтаксис?
static uint8_t write(uint16_t _addr, uint8_t _data); // Implemented elsewhere
static uint8_t read(uint16_t addr); // Implemented elsewhere
#define __GP_REGISTER8(name, address) \
static inline void write##name(uint8_t _data) { \
write(address, _data); \
} \
static inline uint8_t read##name() { \
return read(address); \
}
__GP_REGISTER8 (A, 0x0000); // Register A
__GP_REGISTER8 (B, 0x0001); // Register B
__GP_REGISTER8 (C, 0x0002); // Register C
#undef __GP_REGISTER8
После реализации предыдущих макросов у вас теперь есть возможность совершать следующие вызовы:
writeA(0xA5);
writeB(0x5A);
uint8_t c_value = readC();
Как ##
работать точно (т.е. только в макросах и т. д.)? Кроме того, люди не используют это, потому что это действительно не предлагает никаких преимуществ, и это больше работы, или есть другие побочные эффекты / недостатки использования этой техники?
1 ответ
Это (почти) не о синтаксисе C++, а о языке препроцессора C. Эта функция называется вставкой или объединением токенов. Из руководства GCC:
##
Оператор предварительной обработки выполняет вставку токена. Когда макрос раскрывается, два токена на каждой стороне каждого##
оператор объединяется в один токен, который затем заменяет##
и два оригинальных токена в расширении макроса.
Итак, в вашем (упрощенном) примере
__GP_REGISTER8(A, 0x0000)
расширяется (путем подстановки строк) до
static inline void write ## A(uint8_t _data) { write(0x0000, _data); }
Затем препроцессор заменяет write ## A
тройной с writeA
как вы заметили.
Какие ограничения или проблемы существуют с
##
синтаксис?
Это грязная хитрость макросов, которая может сбить людей с толку, как и вас. Ограничения вполне очевидны: вы можете выполнять вставку токенов - ни меньше, ни больше. Позвольте мне еще раз подчеркнуть, что все это происходит до того, как компилятор C или C++ увидит код. Поэтому, если вы "вызовите" такой макрос с бессмысленными аргументами, препроцессор все равно будет радостно его развернуть, и только тогда компилятор увидит мусор и выведет сообщение об ошибке, в котором мало помощи. Тем не менее, если макрос используется внутри заголовочного файла для очистки повторяющихся объявлений, это, вероятно, хорошо.
Подобные трюки потеряли свою значимость благодаря растущей мощи метапрограммирования шаблонов C++. Можно с полным основанием утверждать, что функциональность, реализованная с помощью опубликованных вами макросов, могла бы быть реализована с использованием шаблонных функций, как в
enum class Registers { A = 0x0000, B = 0x0001, C = 0x0002; };
template<Registers R>
void
write(uint16_t data)
{
write(static_cast<int>(R), data);
}
а затем используется как
write<Registers::A>(0xA5);