4 сентября 2009 в 22:15

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

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  за уточнения.
Maxim Razin @grep0
карма
47,4
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (70)

  • –2
    Во воркэраундах без скобок у вас будет
    а) лишнее копирование объекта, если я не ошибаюсь
    б) если конструктор объявлен как explicit, то B b = A(a) не прокатит вообще. Кажется (давно с++ не трогал)
    • 0
      а) Лишнего копирования не будет
      б) Действительно, с explicit не прокатит. Придётся писать B b = B(A(a)) — кстати, даже в этом случае лишнего копирования не будет. Вот только, если в качестве B выступает длинное шаблонное выражение, писать его два раза как-то очень не хочется
      • 0
        UPD: ошибаюсь, в варианте с дополнительной переменной действительно будет лишнее копирование a.
        • +3
          >> длинное шаблонное…
          typedefы рулят :) правда, писать их тоже лень :)
  • +3
    У Скотта Мейерса в книге «Эффективное использование STL» тоже есть такой неочевидный пример в «Совете №6». Там приведён забавный пример:

    ifstream dataFile(«file.txt»);
    list<int> data(istream_iterator<int>(dataFile), istream_iterator());

    • –1
      Да, там у него из-за жутких наименований типов вообще сложно понять было :)
      • 0
        Интересно, кто-нибудь хоть раз в жизни использовал istream_iterator?
        • +3
          Достаточно часто вижу, удобная штука. А почему, собственно, нет?
          • –3
            название длинное
  • +1
    С появлением C++0x будет ещё один workaround:
    B b{A(i)};
    эту штуку компилятор уж никак не сможет спутать с функцией.
  • +8
    А вообще, я жду не дождусь, когда Бьярн Страуструп издаст мемуары, в которых признается, что разработку с++ ему проплатил ZOG для того, чтобы замучить и без того забитых программистов. Тогда я напишу ему открытое письмо, в котором буду тыкать в него пальцем и смеяться в голос.
  • +1
    Лет 10 уже работаю на С++. Ни разу не писал / не встречал таких невинных кусков кода. Выглядит весьма нечитаемо.
    • НЛО прилетело и опубликовало эту надпись здесь
  • +2
    ОБычно таких вещей элементарно избегаешь из за их сложной читаемости.
    Хотя подкол ещё тот, но из разряда «цирка уродов» — прикольно, страшно, но видишь редко.
    • 0
      Интересно, по С++ сдаются какие-нибудь сертификационные экзамены? Ну, это там где нужно наизусть заучивать приоритеты операторов или все возможные способы употребления массива. Интересно как подобное сдается применительно к С++.
  • 0
    В принципе «ошибку» заметил практически сразу после прочтения кода, но тут я ждал нечто такое, иначе зачем топик? В реальной же ситуации, думаю, прищлось бы искать ошибку, возможно долго и мучительно.
  • 0
    Скажите, я правильно пониманию, что эта «фича» стала следствием того, что во-первых, лишние скобки игнорируются (это в статье), плюс внутри реализаций функций можно объявлять внутренние составные типы (ну классы и структуры к примеру), а вот объявление функции просто «попало под раздачу» как ещё один тип?
    • 0
      Не, это следствие того, что компилятор пытается разобраться по-простому, и самое простое, что у него выходит — это прототип функции. Ну а варнинги — подумаешь, из проектов на с++ их эшелонами вывозить можно.
      • 0
        Я думаю всё же, что если б по стандарту было б запрещено объявлять новые сложные типы внутри реализаций, то повода для этой статьи не было, поскольку бы не пришлось из двух возможных «вы имели ввиду» выбирать ту, которая проще (ну или ещё по какому критерию), а осталось бы одно чёткое понимание того, что имел ввиду разработчик.
        • 0
          А где там объявляется «новый сложный тип», поясните — где и какой тип?
          • +1
            Раз такая пьянка, то давайте договоримся об определениях (сразу извиняюсь: повода считать выс тупым или ещё каким нет, просто обсуждение дальше может не пойти в конструктивном ключе, если сразу не договориться):

            Тип под типом понимается некая сущность, которая описывает «будущую» объявленную переменную.

            Сложный тип — ну может слово «сложный» не совсем уместно, может лучше «ненативный», что переводится как «неродной», т.е. не зашитый непосредственно в язык, т.е. тот тип, который описывает пользователь: сюда относятся классы, структуры, а также объявления функций.

            А теперь ответ на вопрос:
            B b(A(i)); // вот оно объявление сложного типа

            или, как уже было сказано, после убирания лишних скобок:

            B b(A i); // вот оно объявление сложного типа

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

            В С/С++ чаще используется указатели на функции, а именно было:

            B (*b)(A i), которому при предварительной реализации некой функции B ZZZ(A a), можно было ю написать b = ZZZ, а потом вызвать что-то типа b(A a);

            Примеры:
            #include «iostream»
            #include «typeinfo»

            struct A {
            A (double i) { std::cout << 'A' << '\n';}
            };

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

            B ZZZ( A a ) { return B(A(1)); };

            int main () {
            int i = 2;

            B b(A(i));

            b(A(i)); // (2)

            return 0;
            }
            (сразу извините за #include «iostream» — просто хабраредактор такое выдал при испльзовании больше/меньше) Этот пример не будет работать: ошибка линковки, поскольку в (2) какбэ вызывается обработчик, который не имеет реализации (тела). Но это легко поправить например так:
            #include «iostream»
            #include «typeinfo»

            struct A {
            A (double i) { std::cout << 'A' << '\n';}
            };

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

            B ZZZ( A a ) { return B(A(1)); };

            int main () {
            int i = 2;

            B (*b)(A(i)) = ZZZ;

            b(A(i)); // (2)

            return 0;
            }
            Тут всё корректно работает, поскольку мы объявили указатель на функцию и инициализировали его передав ему указатель на ZZZ (аналогия double — тип, double* — указатель на переменную double; B ()(A a) — функция, B (*)(A a) — указатель на функцию).

            Так что вот такие пироги :)
            • 0
              Кстати, кто-нибудь знает хоть одно применение вот таких функций-пустышек, которые объявляются внутри функций [ см. B b(A i) ] кроме засорения кода?
              • 0
                Это прототипы функций (forward declaration), которые будут объявлены позже. Они введены для того, чтобы исключить циклические зависимости. В стандарте это _есть_.
                • 0
                  Это прототипы функций (forward declaration), которые будут объявлены позже.
                  Приведите небольшой работающий пример использования таких прототипов и как они будут «объявлены позже». Вот мне не понятно где это они будут объявлены если область действия b ограничена функцией main(), а насколько мне известно, функции внутри функций делать нельзя, можно только выдавать вот такие прототипы.
                  • +1
                    Да легко (пояснение: A, B — некоторые классы\структуры):

                    void someFunc()
                    {
                        B b(A a); // если закомментировать эту строку, не будет линковаться

                        A a;
                        b(a);
                    }

                    B b(A a)
                    {
                        // do something and return B
                    }
                    • 0
                      Действительно работает! Тока надо было реализацию функции за пределы той функции, в которой происходит задание прототипа B b(A a); вытянуть и я бы тогда сам разобрался (это я себе всё говорил). Всё теперь понял, спасибо. Просто завис на том, что объявление происходит внутри области видимости некой функции, а внутри функций писать реализацию других функций незя. Огромное спасибо ещё раз. Пересмотрю некоторое своё видение в данном вопросе, хотя такой изврат навряд ли применять когда-нить буду.
                      • +1
                        Это не изврат, это нормальный способ разрешения зависимостей (нормальный для с++) :) В принципе, прототип можно и из функции вытащить наверх, но зачем, если нужно сослаться только тут. В этом подходе функции аналогичны неймспейсам в плане ограничения видимости прототипов/символов.
                        • 0
                          Очередное спасибо. Этим коментом вы ответили на мой другой вопрос (если конечно теперь немного перефразировать с учётом полученных новых знаний).
                          • 0
                            Совершенно не за что. Я на сях давно уже не пишу, еще с тех пор, как boost не имел отношения к stl вообще, но базу помню, так что спрашивайте, ежели чего.
              • 0
                По поводу применения — большинство сишных хедеров состоят как раз из таких прототипов.
              • 0
                Кажется, в старые юниксовские времена, так даже было принято писать… Во всяком случае, я видел такие исходники, там, вместо того чтобы сделать обычный #include «header», они прямо перед использованием функции её объявляют, и тут же дёргают :). Вроде такого:
                    ...
                    int foo(int a);
                    int res = foo(10);
                    ...
                }
            • 0
              >> Тип под типом понимается некая сущность, которая описывает «будущую» объявленную переменную.

              Нихрена себе. Так тип — это тип или будущая переменная? Мне всегда казалось, что по отношению к языку С/С++ слово «тип» имеет вполне определенный смысл, и не надо его переопределять.

              >> B b(A(i)); // вот оно объявление сложного типа
              Это НЕ объявление сложного типа. Тут, правда, можно придраться к слову «объявление» — definition .vs. declaration, но не суть :) Это — прототип конкретной функции. Не типа, а конкретной сущности, с привязкой по ее имени и сигнатуре. «Типами» и не пахнет :)
              • 0
                Нихрена себе. Так тип — это тип или будущая переменная?
                Тип — это то, что описывает переменную, но ещё не переменная, в общем случае, по типу нельзя обратиться к чему либо внутри него (static и прочие подобные вещи не в счёт) и вообще что-то с ним делать — приходится создавать переменную или выделять память через new и пр.

                Зачем я ввел вначале определения? По опыту общения с разными разработчиками, которые работали с разными языками, знаю, что порой они переносят определения похожих понятий из одного языка в другой и порой происходит недопонимание, когда кто-то использует слово, подразумевая некий другой смысл (опять же может и похожий, но черти, как известно, в деталях). Так один из разработчиков на дух не переносил называние класса или структуры типом — под типами он всегда подразумевал только простые, зашитые непосредственно в язык (double, int и пр.).

                B b(A(i)); // вот оно объявление сложного типа
                Эта запись аналог например такой:
                class
                {
                public:
                double ttt;
                } m;
                Т.е. в данном случае происходит описание некоего неименнованого типа и происходит сразу создание переменной m этого неименованого типа. Для функции просто работает аналогия. Если неправ — тыкните в какое-нибудь описание или сами что-нить отпишите.
                • 0
                  Насчет типа понял, ок. Но позволю себе поправить вас — никаких переменных там не создается. Ссылка тут: habrahabr.ru/blogs/cpp/68796/#comment_1954106 и тут: habrahabr.ru/blogs/cpp/68796/#comment_1953851

                  Как уже сказал, никакая переменная не создается. Здесь объявляется прототип функции — т.е., символ и сигнатура, что такая функция существует, и ее тело будет определено позже в коде / в другом файле. Это необходимо, потому что с/с++ — однопроходный компилятор, и результат компиляции будет зависеть от того, в каком порядке расположены объекты в файле (модуле в терминологии с++). Чтобы иметь возможность обращаться к тем сущностям, которые описаны позже в файле, и введены forward declarations для функций и пользовательских типов (т.е. можно написать так: struct SomeName, использовать ее, а саму структуру описать в конце файла)
                • 0
                  >> Эта запись аналог например такой:
                  Ну вы уже поняли, что не аналог.
                  • 0
                    спасибо, разобрался. Ваш коммент выше внёс ясность, теперь есть пища для размышлений, новых проб и ошибок :)
        • –2
          если бы все в мире было по стандарту)
      • 0
        -Wall.
  • +5
    Визуал хитрый:
    warning C4930: 'B b(A)': prototyped function not called (was a variable definition intended?)

    Хотя он порой такое компилит, что волосы дыбом становятся — как у него это вышло.
    • –1
      Какой уровень надо включить, чтобы этот ворнинг появился? А то на /W4 у VC начинается совершенно параноидальное поведение и он начинает материться на стандартные идиомы. У меня сейчас под рукой нет вижуала, но некоторые перлы помню:

      while(true) { blablabla… break… blablabla } // Ругается на константное выражение в while

      char x='a';
      char z=toupper(x); // Ругается на присвоение int'а char'у

      В общем, когда количество выключенных ворнингов в проекте подходит к десятку, начинаешь сомневаться в адекватности мелкомягких.
      • НЛО прилетело и опубликовало эту надпись здесь
      • 0
        /W3 — по умолчанию стоит, с ним так и матерится
  • 0
    С каких это пор стандарт начал разрешать вложенные определения функций? Вы бы хоть версии компиляторов называли, когда такое постите…
    • 0
      Извиняюсь, версию вижу в тексте.
    • +2
      Это не вложенное определение, а объявление внешней функции. Например, можно написать в одном модуле

      int foo() { return 42; }

      а в другом

      int bar(int x) {
      int foo();
      return foo()+x;
      }

      Есть везде, начиная с древнейших диалектов C.
  • –1
    Эх. Не знаю как кому, а мне статья самомнение повысила ))) автор, спасибо. Немного радости перед сном не помешает.
  • НЛО прилетело и опубликовало эту надпись здесь
    • +4
      C++FQA — очень странный и предвзятый ресурс, я бы назвал его карманным справочником по троллингу для C++ненавистников. Хотя местами комментарии неплохо и по делу дополняют FAQ.

      Ну а статью в хсакепе, в которой аффтар не видит разницы между C и C++ и не понимает, зачем может понадобится портабельный ассемблер, даже местная публика заминусовала ниже плинтуса.
      • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Страуструп говорил: есть два вида языков — те, которые все ругают, и те, на которых никто не программирует.

      Как большой любитель С++ я вижу, что его многие недостатки вызываны объективными факторами, а при умелом использовании языка попросту не видны.

      Если даже взять пример из данного поста — я никогда не пишу B b(A(i)), всегда только B b = A(i). Привычка. Если бы у автора поста была бы та же привычка, он бы на грабли не наткнулся.
      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          Он не «сделан так», он «вырулил туда». Все языки эволюционируют в сторону сложности и расширения синтаксиса, при этом неизбежно появляются какие-то «паразиты». Сравните, к примеру, первые и последние версии Бейсика, Джавы, Сишарпа.
          • +3
            С++ в современном виде действительно во многом сделан странной политикой комитета в 1990-е, когда в стандарт вносились совершенно необкатанные (а иногда даже и ни разу не реализованные) фичи. В результате получились страннейшие сайд-эффекты, например, подъязык темплейтов случайно оказался полным по Тьюрингу — полезнейшая вещь, но если бы его проектировали сейчас, не пришлось бы так много делать через задницу. А export так никто и не научился поддерживать, кроме довольно маргинального Comeau.

            Мне как-то больше нравится более консервативный подход к стандартизации, практикуемый, например, при принятии RFC. Чтобы что-то хотя бы начали рассматривать как кандидата на включение в стандарт, необходимо предъявить работоспособную имплементацию.
            • 0
              Возможно, хотя стандартизация — штука тонкая, вероятно, здесь нет «лучшего» решения. В качестве отрицательного примера можно привести сверхконсервативную стандартизацию языка Ада. Сначала была Ада 83, потом Ада 95, в которой появились какие-то классы и вообще претензия на ООП. Господа, повторюсь, в 1995 году! (Шаблоны, надо признать, там в некотором виде уже имеются).
        • 0
          Это вы ещё перл не пробовали. Там основной принцип — There's more than one way to do it
    • 0
      Да, надо ругать создателей грабель за то что на них наступаешь. И рыхлить землю руками…
      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          Вы рыхлите землю лопатой? O_O
          • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Это всё конечно хорошо :), но какова замена? На чём другом писать высокопроизводительный софт?

      Критиковать все горазды…
      • НЛО прилетело и опубликовало эту надпись здесь
  • НЛО прилетело и опубликовало эту надпись здесь
    • +1
      потому что это простой и удобный способ не вводить дополнительную переменную A, если она больше никогда в жизни не пригодится. Точнее, был бы простым и удобным, если бы оно работало. А от лишних скобок это выглядит уродливо.
      • НЛО прилетело и опубликовало эту надпись здесь
        • +1
          «Читабельность кода имеет куда более высокий приоритет. „
          Ну так я о том же ))
          Например, API требует передать в конструктор какой-нибудь объект типа A. Вы понятия не имеете, что такое этот тип A, нафиг он нужен конструктору, и уж точно уверены что сами никогда его использовать не будете. Так можно было бы загнать этот A прямо в вызов, и не помещать в код лишнюю мусорную переменную. Если таких вызовов конструкторов будет много (например, при реализации listener'ов для GUI), то в коде могут оказаться тонны мусорных переменных.
          м?
          • НЛО прилетело и опубликовало эту надпись здесь
  • +1
    Ужас!

    Кстати, студия (2008) на такой код даёт вполне вменяемый ворнинг: «warning C4930: 'B b(A)': prototyped function not called (was a variable definition intended?)»
  • 0
    Лучший вариант всё же:
    A a(i);
    B b(a);
    Дабы избегать такой «магии» да и выделять одному действию одну строку. Иногда даже в code conventions пишут такое. В action script встречал что-то похожее: нужное мне действие корректно выполнялось точль после введения дополнительной переменной.

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