Какие применения оператора препроцессора ## и какие подводные камни следует учитывать?

Как упоминалось во многих из моих предыдущих вопросов, я работаю через K&R и сейчас занимаюсь препроцессором. Одна из наиболее интересных вещей - чего я никогда раньше не знал из своих предыдущих попыток изучить C - это оператор препроцессора ##. Согласно K&R:

The preprocessor operator ## provides a way to concatenate actual arguments during macro expansion. If a parameter in the replacement text is adjacent to a ##, the parameter is replaced by the actual argument, the ## and surrounding white space are removed, and the result is re-scanned. For example, the macro paste concatenates its two arguments:

#define paste(front, back) front ## back

so paste(name, 1) creates the token name1.

Как и зачем кому-то это использовать в реальном мире? Каковы практические примеры его использования и есть ли подводные камни, которые следует учитывать?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
89
0
70 580
13
Перейти к ответу Данный вопрос помечен как решенный

Ответы 13

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

CrashRpt: использование ## для преобразования многобайтовых строк макросов в Unicode

Интересное использование в CrashRpt (библиотеке отчетов о сбоях) следующее:

#define WIDEN2(x) L ## x
#define WIDEN(x) WIDEN2(x)
//Note you need a WIDEN2 so that __DATE__ will evaluate first.

Здесь они хотят использовать двухбайтовую строку вместо однобайтовой строки на символ. Возможно, это выглядит бессмысленным, но на то есть веская причина.

 std::wstring BuildDate = std::wstring(WIDEN(__DATE__)) + L" " + WIDEN(__TIME__);

Они используют его с другим макросом, который возвращает строку с датой и временем.

Размещение L рядом с __ DATE __ приведет к ошибке компиляции.


Windows: использование ## для универсальных строк Unicode или многобайтовых строк

Windows использует что-то вроде следующего:

#ifdef  _UNICODE
    #define _T(x)      L ## x
#else
    #define _T(x) x
#endif

И _T используется везде в коде


Различные библиотеки, используемые для чистых имен аксессуаров и модификаторов:

Я также видел, как он используется в коде для определения аксессуаров и модификаторов:

#define MYLIB_ACCESSOR(name) (Get##name)
#define MYLIB_MODIFIER(name) (Set##name)

Точно так же вы можете использовать этот же метод для любых других типов создания умных имен.


Различные библиотеки, использующие его для одновременного объявления нескольких переменных:

#define CREATE_3_VARS(name) name##1, name##2, name##3
int CREATE_3_VARS(myInts);
myInts1 = 13;
myInts2 = 19;
myInts3 = 77;

Поскольку вы можете объединить строковые литералы во время компиляции, вы можете уменьшить выражение BuildDate до std::wstring BuildDate = WIDEN(__DATE__) L" " WIDEN(__TIME__); и неявно построить сразу всю строку.

user666412 15.02.2016 20:54

Я использую его для добавления пользовательских префиксов к переменным, определяемым макросами. Так что-то вроде:

UNITTEST(test_name)

расширяется до:

void __testframework_test_name ()

Вы можете использовать вставку токена, когда вам нужно объединить параметры макроса с чем-то другим.

Его можно использовать для шаблонов:

#define LINKED_LIST(A) struct list##_##A {\
A value; \
struct list##_##A *next; \
};

В этом случае LINKED_LIST (int) даст вам

struct list_int {
int value;
struct list_int *next;
};

Точно так же вы можете написать шаблон функции для обхода списка.

Это полезно во всех ситуациях, чтобы не повторяться без нужды. Ниже приводится пример исходного кода Emacs. Мы хотели бы загрузить ряд функций из библиотеки. Функция "foo" должна быть присвоена fn_foo и так далее. Мы определяем следующий макрос:

#define LOAD_IMGLIB_FN(lib,func) {                                      \
    fn_##func = (void *) GetProcAddress (lib, #func);                   \
    if (!fn_##func) return 0;                                           \
  }

Затем мы можем использовать его:

LOAD_IMGLIB_FN (library, XpmFreeAttributes);
LOAD_IMGLIB_FN (library, XpmCreateImageFromBuffer);
LOAD_IMGLIB_FN (library, XpmReadFileToImage);
LOAD_IMGLIB_FN (library, XImageFree);

Преимущество состоит в том, что не нужно записывать одновременно fn_XpmFreeAttributes и "XpmFreeAttributes" (и есть риск опечатки в одном из них).

Основное использование - это когда у вас есть соглашение об именах и вы хотите, чтобы ваш макрос использовал это соглашение об именах. Возможно, у вас есть несколько семейств методов: image_create (), image_activate () и image_release (), а также file_create (), file_activate (), file_release () и mobile_create (), mobile_activate () и mobile_release ().

Вы можете написать макрос для обработки жизненного цикла объекта:

#define LIFECYCLE(name, func) (struct name x = name##_create(); name##_activate(x); func(x); name##_release())

Конечно, своего рода «минимальная версия объектов» - не единственный вид соглашения об именах, к которому это применимо - почти в подавляющем большинстве соглашений об именах используется общая подстрока для формирования имен. Это могут быть имена функций (как указано выше), имена полей, имена переменных или что-то еще.

Я использую его в программах на C, чтобы помочь правильно применить прототипы для набора методов, которые должны соответствовать какому-то соглашению о вызовах. В некотором смысле, это можно использовать для объектной ориентации бедняков по прямой C:

SCREEN_HANDLER( activeCall )

расширяется примерно до этого:

STATUS activeCall_constructor( HANDLE *pInst )
STATUS activeCall_eventHandler( HANDLE *pInst, TOKEN *pEvent );
STATUS activeCall_destructor( HANDLE *pInst );

Это обеспечивает правильную параметризацию для всех "производных" объектов, когда вы:

SCREEN_HANDLER( activeCall )
SCREEN_HANDLER( ringingCall )
SCREEN_HANDLER( heldCall )

указанное выше в ваших файлах заголовков и т. д. Это также полезно для обслуживания, если вы даже захотите изменить определения и / или добавить методы к «объектам».

SGlib использует ## для подделки шаблонов в C. Поскольку нет перегрузки функций, ## используется для вставки имени типа в имена сгенерированных функций. Если бы у меня был тип списка с именем list_t, я бы получил функции с именем sglib_list_t_concat и так далее.

Это очень полезно для ведения журнала. Ты можешь сделать:

#define LOG(msg) log_msg(__function__, ## msg)

Или, если ваш компилятор не поддерживает функция и func:

#define LOG(msg) log_msg(__file__, __line__, ## msg)

Вышеупомянутые "функции" регистрируют сообщение и показывают, какая именно функция зарегистрировала сообщение.

Мой синтаксис C++ может быть не совсем правильным.

Что вы пытались с этим сделать? Он также будет работать без символа "##", поскольку нет необходимости вставлять токен "в" msg ". Вы пытались преобразовать сообщение в строку? Кроме того, ФАЙЛ и ЛИНИЯ должны быть в верхнем, а не в нижнем регистре.

bk1e 20.10.2008 00:44

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

ya23 12.12.2008 12:59

Вот проблема, с которой я столкнулся при обновлении до новой версии компилятора:

Ненужное использование оператора вставки токена (##) не переносимо и может генерировать нежелательные пробелы, предупреждения или ошибки.

Когда результат оператора вставки токена не является допустимым токеном препроцессора, оператор вставки токена не нужен и, возможно, вреден.

Например, можно попытаться построить строковые литералы во время компиляции с помощью оператора вставки токена:

#define STRINGIFY(x) #x
#define PLUS(a, b) STRINGIFY(a##+##b)
#define NS(a, b) STRINGIFY(a##::##b)
printf("%s %s\n", PLUS(1,2), NS(std,vector));

В некоторых компиляторах это выдаст ожидаемый результат:

1+2 std::vector

В других компиляторах это будет включать нежелательные пробелы:

1 + 2 std :: vector

Довольно современные версии GCC (> = 3.3 или около того) не смогут скомпилировать этот код:

foo.cpp:16:1: pasting "1" and "+" does not give a valid preprocessing token
foo.cpp:16:1: pasting "+" and "2" does not give a valid preprocessing token
foo.cpp:16:1: pasting "std" and "::" does not give a valid preprocessing token
foo.cpp:16:1: pasting "::" and "vector" does not give a valid preprocessing token

Решение состоит в том, чтобы опустить оператор вставки токена при объединении токенов препроцессора с операторами C / C++:

#define STRINGIFY(x) #x
#define PLUS(a, b) STRINGIFY(a+b)
#define NS(a, b) STRINGIFY(a::b)
printf("%s %s\n", PLUS(1,2), NS(std,vector));

Глава документации GCC CPP о конкатенации содержит более полезную информацию об операторе вставки токена.

Спасибо - я не знал об этом (но тогда я не слишком часто использую эти операторы предварительной обработки ...).

Michael Burr 20.10.2008 05:12

Он не зря называется оператором «вставки токена» - цель состоит в том, чтобы получить единственный токен, когда вы закончите. Хорошая рецензия.

Mark Ransom 30.04.2009 22:17

Когда результат оператора вставки токена не является допустимым токеном препроцессора, поведение не определено.

alecov 30.06.2014 19:19

Изменения языка, такие как шестнадцатеричные числа с плавающей запятой или (в C++) разделители цифр и определяемые пользователем литералы, постоянно меняют то, что составляет «действительный токен предварительной обработки», поэтому, пожалуйста, никогда злоупотребляет этим! Если вам нужно разделить (собственно язык) токены, пожалуйста, пишите их как два отдельных токена и не полагайтесь на случайное взаимодействие между грамматикой препроцессора и языком как таковым.

Kerrek SB 16.10.2016 16:06

Я использую его для домашнего свернутого утверждения на нестандартном компиляторе C для встроенных:



#define ASSERT(exp) if (!(exp)){ \
                      print_to_rs232("Assert failed: " ## #exp );\
                      while(1){} //Let the watchdog kill us 

Я так понимаю, что вы имеете в виду под «нестандартным», что компилятор не выполнял вставку строк, а выполнял вставку токенов - или он работал бы даже без ##?

PJTraill 10.06.2015 13:44

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

Связь

Мой ответ на этот вопрос показал, как применение небольшой магии препроцессора позволяет вам определять ваше перечисление следующим образом (например) ...;

ENUM_BEGIN( Color )
  ENUM(RED),
  ENUM(GREEN),
  ENUM(BLUE)
ENUM_END( Color )

... С тем преимуществом, что расширение макроса не только определяет перечисление (в файле .h), но также определяет соответствующий массив строк (в файле .c);

const char *ColorStringTable[] =
{
  "RED",
  "GREEN",
  "BLUE"
};

Имя таблицы строк происходит от вставки параметра макроса (т. Е. Color) в StringTable с помощью оператора ##. В подобных приложениях (уловках?) Операторы # и ## неоценимы.

При использовании операторов предварительной обработки token-paste ('##') или преобразования строк ('#') следует помнить, что вам нужно использовать дополнительный уровень косвенного обращения, чтобы они работали должным образом во всех случаях.

Если вы этого не сделаете и элементы, переданные оператору вставки токена, сами являются макросами, вы получите результаты, которые, вероятно, не будут тем, что вам нужно:

#include <stdio.h>

#define STRINGIFY2( x) #x
#define STRINGIFY(x) STRINGIFY2(x)
#define PASTE2( a, b) a##b
#define PASTE( a, b) PASTE2( a, b)

#define BAD_PASTE(x,y) x##y
#define BAD_STRINGIFY(x) #x

#define SOME_MACRO function_name

int main() 
{
    printf( "buggy results:\n");
    printf( "%s\n", STRINGIFY( BAD_PASTE( SOME_MACRO, __LINE__)));
    printf( "%s\n", BAD_STRINGIFY( BAD_PASTE( SOME_MACRO, __LINE__)));
    printf( "%s\n", BAD_STRINGIFY( PASTE( SOME_MACRO, __LINE__)));

    printf( "\n" "desired result:\n");
    printf( "%s\n", STRINGIFY( PASTE( SOME_MACRO, __LINE__)));
}

Выход:

buggy results:
SOME_MACRO__LINE__
BAD_PASTE( SOME_MACRO, __LINE__)
PASTE( SOME_MACRO, __LINE__)

desired result:
function_name21

Для объяснения этого поведения препроцессора см. stackoverflow.com/questions/8231966/…

Adam Davis 02.12.2011 00:50

@MichaelBurr, я читал твой ответ, и у меня есть сомнения. Почему этот ЛИНИЯ печатает номер строки?

HELP PLZ 26.06.2014 20:51

@AbhimanyuAryan: Я не уверен, что вы спрашиваете об этом, но __LINE__ - это специальное имя макроса, которое препроцессор заменяет текущим номером строки исходного файла.

Michael Burr 26.06.2014 23:16

Было бы здорово, если бы спецификации языка могли быть процитированы / связаны, как в здесь

Antonio 19.04.2017 17:02

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

#define BITFMASK(bit_position) (((1U << (bit_position ## _WIDTH)) - 1) << (bit_position ## _LEFTSHIFT))

При определении описания бита регистра мы делаем следующее:

#define ADDR_LEFTSHIFT                          0

#define ADDR_WIDTH                              7

А при использовании BITFMASK просто используйте:

BITFMASK(ADDR)

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