Блог пользователя LoDThe

Автор LoDThe, история, 7 лет назад, По-русски

Компилятор GNU G++ 5.1.0 не компилирует следующий код:

#include <vector>

using namespace std;

vector <pair <int, int>> a(0);

int main()
{
	return 0;
}

Однако успешно компилирует следующий код:

#include <vector>

using namespace std;

#define pii pair <int, int>

vector <pii> a(0);

int main()
{
	return 0;
}

Почему 2 код компилируется, хотя после "pii" нет пробела? Особая обработка defin'ов?

  • Проголосовать: нравится
  • +7
  • Проголосовать: не нравится

»
7 лет назад, # |
  Проголосовать: нравится +18 Проголосовать: не нравится

Думаю, да, особая обработка define'ов. Проблема с >> идёт (шла?) на уровне разбора файла на лексемы: >> в зависимости от контекста должно быть либо двумя лексемами (в случае шаблонов), либо одной (побитовый сдвиг). Из-за этого непонятно. А вот когда есть #define, то всё понятно, потому что лексемы #define так просто не скеливает.

Кстати, в C++ вместо #define для типов принято использовать typedef. Использовать так: представляем, что нам нужно объявить переменную, объявляем, а имя переменной заменяем на новое имя типа, перед объявлением дописываем typedef. Например:

pair<int, int> my_pair;
// Заменяем на
typedef pair<int, int> pii;

Смысл: сложнее выстрелить себе в ногу. Например, если вы сделаете #define PCHAR char*, то конструкция PCHAR a, b развернётся макросами в char* a, b, что на самом деле значит char *a, b, т.е. a будет указателем, а b — просто char. Похожие проблемы (уже не в олимпиадах) возникают с const, и другими сложными типами (вроде указателя на функцию).

  • »
    »
    7 лет назад, # ^ |
      Проголосовать: нравится +5 Проголосовать: не нравится

    Спасибо за ответ. Но "оборачивание" целых функций в define нежелательно? Функции стоит оформлять "стандартно"?

    • »
      »
      »
      7 лет назад, # ^ |
        Проголосовать: нравится +16 Проголосовать: не нравится

      Тоже верно. #define вообще стоит использовать по минимуму. Классический пример — функция min:

      #define min(a, b) ((a) < (b) ? (a) : (b))
      

      А теперь представим, что у нас есть функция go, которая что-нибудь перебирает:

      int answer = min(go(1), go(2));
      // Заменится на
      int answer = go(1) < go(2) ? go(1) : go(2);
      

      И вот тут мы получим, что функция была вызвана дважды, а не трижды. Аналогичный пример:

      int x = min(a++, b);
      // Заменится на
      x = a++ < b ? a++ : b;
      

      И вот тут мы в одном из случаев получаем не просто возможность увеличения a на двойку, а хуже — неопределённое поведение, так как переменная не имеет права меняться дважды в одной операции (привет от ++i + ++i, это даже на лурке есть).

»
7 лет назад, # |
  Проголосовать: нравится -16 Проголосовать: не нравится

I tried this on ideone:

#include <vector>

using namespace std;

#define v vector<pair<int, int>>

v a(0);

int main()
{
	return 0;
}

It doesn't compile with C++ 5.1 but does with C++14. Interesting, please someone explain this, too :D Is it some sort of smart compiler optimization?

»
7 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

This is off the top of my head, so the details may not be 100% precise.

Older versions of C++ standard (standard itself, not compilers) don't allow writing >> without a space when nesting templates because it makes parsing harder (">>" looks just like the operator ">>"). The compiler may compile such code (as is the case with any undefined behavior), but it doesn't have to.

Newer versions of C++ standard (IIRC, already C++11?) allow omitting the space.

»
7 лет назад, # |
Rev. 2   Проголосовать: нравится +1 Проголосовать: не нравится

Here is how programming language parsing often works. Some programs may do it differently or have some exceptions. C++ is extra nasty in this sense and blurs the border between the steps, but general idea remains.

Compiler/interpreter frontends have 2 stages:

  1. lexing — transform sequence of characters into sequence of tokens
  2. parsing — transform sequence of tokens into abstract syntax tree or directly to bytecode

Instead of working with sequences of characters directly parsers often work with sequences of tokens. Each token is sequence of one or more symbols. One token could be math operator + *=, identifier main i myFunction, constant 3 "abc" True, special keyword if else. This has multiple benefits two simpler languages instead of one complicated, more easily ignore whitespaces, alternative character sequences for the same token (and==&&), hacks like handling python indentation in smart lexer.

Lexers are usually simple and don't know much about the whole program instead they try to greedily create one of the tokens from not knowing if it form a correct program or not.

In this case we are interested in these tokens (real C++ is more complicated than that, but it's good enough for explaining):

  • IDENTIFIER: [a-zA-Z]+
  • R_SHIFT >>
  • LT: <
  • GT: >

Templates in <=C++03 expected something like IDENTIFIER LT argument_list GT. R_SHIFT is clearly not the same as GT so it causes parsing error. C++11 changed the rules to allow >> for ending template, but it makes formal language description even more complicated.

As for why second version of code doesn't fail. In c++ preprocessor works with sequences of tokens not sequences of characters and after being recognized as two separate tokens they aren't combined unless special token concatenation operator ## is used.

It means that character sequence vector<pii> first gets split into tokens identifier(vector) < identifier(pii) >. In the next step preprocessor recognizes that identifier pii is macro and replaces it with matching sequence of tokens identifier(vector) < int >. Parser doesn't know if two > tokens came from two space separated '>' character or macro expansion so they are treated the same.

Here are some examples which are not affected by c++11 changes in template parsing.

    cout << (8 >> 1) << "\n";  // 8 right shift 1 -> 4
    cout << (8 > > 1) << "\n";  // Doesn't compile 8 greater than greater than 1 does't make sense.
#define A >
    cout << (8 A> 1) << "\n";  // Doesn't compile. There is no space, but A> gets recognized as 2 tokens before macro expansion.
#define B(a) a>
    cout << (8 B(>) 1) << "\n"; // Doesn't compile, 2 tokens.
#define C(a) a ## >
    cout << (8 C(>) 1) << "\n";  // 4. ## operator concatenates 2 greater than tokens in to right shift.
#define D J(>,
#define J(a, b) a ## b
    cout << (8 D>) 1) << "\n";  // 4
    cout << (8 D >) 1) << "\n";  // 4 Same as above.