Расширение макроса с одинарным минусом

Рассмотрим следующий код:

#define A -100

//later..
void Foo()
{
  int bar = -A;
  //etc..
}

Теперь это прекрасно компилируется на некоторых основных протестированных мной компиляторах (MSVC, GCC, Clang) и bar == 100 как и ожидалось, это потому, что препроцессоры всех этих компиляторов вставляют пробел между токенами, так что вы получите:

int bar = - -100;

Поскольку я хотел бы, чтобы мой код был максимально переносимым, я решил проверить, определяется ли это поведение стандартом, но я ничего не могу найти в нем. Это поведение гарантировано стандартом или это просто функция компилятора и является наивным подходом (который явно не компилируется) bar = --100; разрешено тоже?

Ответ 1

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

Это отсутствие конкатенации обеспечивается тем, что исходные файлы должны быть проанализированы: расширение макроса выполняется в фазе перевода 4. Перед этой фазой перевода, во время фазы преобразования 3, исходный файл должен быть преобразован в последовательность токенов предварительной обработки и пробелов [ lex.phases]/3:

Исходный файл разлагается на токены предварительной обработки и последовательности символов пробела (включая комментарии). Исходный файл не должен заканчиваться маркером частичной предварительной обработки или частичным комментарием .13 Каждый комментарий заменяется одним пробелом. Символы новой строки сохраняются. Сохраняется ли каждая непустая последовательность символов пробела, кроме новой строки, или заменяется одним символом пробела, не уточняется.

Таким образом, после фазы перевода 3 последовательность токенов рядом с определением бара может выглядеть так:

// here {...,...,...} is used to list preprocessing tokens.
{int, ,bar, ,=, ,-,A,;}

Тогда после фазы 4 вы получите:

{int, ,bar, ,=, ,-,-, ,100,;}

Пространство концептуально удалено на этапе 7:

{int,bar,=,-,-,100,;}

Ответ 2

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

После завершения предварительной обработки компилятор сам проанализирует код в терминах предварительно проанализированных токенов предварительной обработки. Собственно компилятор не будет пытаться объединить два соседних токена в один токен.

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

Например

#define M1(a, b) a-b
#define M2(a, b) a##-b

int main()
{
  int i = 0;
  int x = M1(-, i); // interpreted as 'int x = -(-i);'
  int y = M2(-, i); // interpreted as 'int y = --i;' 
}

Вот как спецификация языка определяет поведение.

В практических реализациях этап предварительной обработки и этап компиляции обычно отделены друг от друга. А выходные данные этапа предварительной обработки обычно представлены в виде простого текста (а не в виде некоторой базы данных токенов). В таких реализациях препроцессор и собственно компилятор должны договориться о некотором соглашении о том, как разделять смежные ("касающиеся") токены предварительной обработки. Обычно препроцессор вставляет дополнительный пробел между двумя отдельными токенами, которые "касаются" в исходном коде.

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

Обратите внимание, что, поскольку это пространство "не должно быть там", такие реализации также должны будут приложить некоторые усилия для обеспечения того, чтобы это дополнительное пространство было "необнаружимым" в других контекстах. Например

#define M1(a, b) a-b
#define M2(a, b) a##-b

#define S_(x) #x
#define S(x) S_(x)

int main()
{
  std::cout << S(M1(-, i)) << std::endl; // outputs '--i'
  std::cout << S(M2(-, i)) << std::endl; // outputs '--i'
}

Предполагается, что обе строки main будут выводить --i.

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