Pull to refresh

Неочевидная особенность в синтаксисе определения переменных

C++ *
Предлагается совершенно невинный на вид кусок кода на C++. Здесь нет ни шаблонов, ни виртуальных функций, ни наследования, но создатели этого чудесного языка спрятали грабли посреди чистa поля.

struct A {
  A (int i) {}
};

struct B {
  B (A a) {}
};

int main () {
  int i = 1;
  B b(A(i)); // (1)
  return 0;
}


* This source code was highlighted with Source Code Highlighter.


Вопрос: какой тип у переменной b? Совсем не тот, который можно было бы предположить на первый взгляд.


Анализ


Конечно же, тип переменной b не B, иначе бы не было этой статьи :) Я не буду сразу приводить ответ, а вместо этого расскажу, как до него можно дойти, не копаясь в тысячестраничном стандарте.

Для начала добавим немного отладочной печати:
#include <iostream>
struct A {
  A (int i) { std::cout << 'A';}
};

struct B {
  B (A a) { std::cout << 'B';}
};

int main () {
  int i = 1;
  B b(A(i)); // (1)
  return 0;
}

* This source code was highlighted with Source Code Highlighter.


Если попробовать запустить этот код, окажется, что вообще ничего не выводится. Но если заменить строку (1) на
  B b(A(1));

внезапно всё начинает работать.

А теперь посмотрим внимательно на вывод компилятора при максимально включенных предупреждениях
$ g++ -W -Wall test.cpp
x.cpp:2: warning: unused parameter ‘i’
x.cpp:6: warning: unused parameter ‘a’
x.cpp: In function ‘int main()’:
x.cpp:10: warning: unused variable ‘i’

С первыми двумя строками всё понятно, действительно параметры конструкторов не используются. А вот последняя строка выглядит очень странно. Как переменная i оказалась неиспользуемой, если она используется в следующей строке?

В принципе, этой информации достаточно, чтобы, немного подумав, ответить на поставленный вопрос. Но если умные мысли в голову не приходят, и хочется ещё немного поприключаться, почему бы просто не спросить компилятор? На помощь приходит RTTI.

#include <iostream>
#include <typeinfo>

struct A {
  A (int i) {}
};

struct B {
  B (A a) {}
};

int main () {
  int i = 1;
  B b(A(i)); // (1)
  std::cout << typeid(b).name() << std::endl;
  return 0;
}

* This source code was highlighted with Source Code Highlighter.


При компиляции GCC 4.3 результатом выполнения этой программы является строка
F1B1AE

в которой зашифрована нужная нам информация о типе переменной (конечно, другой компилятор выдаст другую строку, формат вывода type_info::name() в стандарте не описан и оставлен на усмотрение разработчика). Узнать же, что означают эти буквы и цифры, нам поможет c++filt.
$ c++filt -t F1B1AE
B ()(A)

Вот и ответ: это функция, принимающая на вход параметр типа A и возвращающая значение типа B.

Причина


Осталось понять, почему наша строка проинтерпретировалась таким неожиданным способом. Всё дело в том, что в объявлении типа переменной лишние скобки вокруг имени игнорируются. Например, мы можем написать
  int (v);

и это будет означать в точности тоже самое, что
  int v;


Поэтому многострадальную строку (1) можно без изменения смысла переписать, убрав лишнюю пару скобок:
  B b(A i);

Теперь невооружённым взглядом видно, что b это объявление функции с одним аргументом типа A, которая возвращает значение типа B.

Заодно мы объяснили странный ворнинг о неиспользованной переменной i — действительно, она не имеет никакого отношения к формальному параметру i.

Workarounds


Нам осталось только объяснить компилятору, что же на самом деле мы от него хотим — то есть, получить переменную типа B, проинициализированную переменной типа A. Самый простой способ — добавить лишних скобок, вот так:
  B b((A(i)));

или так:
  B b((A)(i));

Этого достаточно, чтобы убедить парсер, что это не объявление функции.

Как альтернативу, можно использовать форму вызова конструктора с помощью присваивания, если только конструктор не объявлен explicit:
  B b = A(i);

Несмотря на наличие знака '=', никакого лишнего копирования здесь не происходит, в чём можно легко убедиться, заведя в классе B приватный конструктор копирования.

А можно просто ввести дополнительную переменную:
  A a(i);
  B b(a);

Правда, при этом потребуется лишнее копирование переменной a, но во многих случаях это приемлемо.

Выберите тот способ, который кажется вам более понятным :)

Навеяно постом в StackOverflow

P.S. Спасибо sse  за уточнения.
Tags:
Hubs:
Total votes 87: ↑75 and ↓12 +63
Views 5.9K
Comments 70
Comments Comments 70