Темные углы C++

Я нашел таки для себя ответ на вопрос про “лишние скобки” вокруг параметра, задающего интервальный итератор (см. “Скоростное чтение файла в STL через итераторы”). Например:

std::ifstream is("testfile.txt");
std::string val(
  (std::istream_iterator<char>(is)),
  std::istream_iterator<char>()
);

Скотт Мейерс в книге “Эффективное использование STL. Библиотека программиста” в Совете 6 “Остерегайтесь странностей лексического разбора C++” (стр. 42, изд. “Питер” 2002) дает исчерпывающее объяснение этого “феномена”. Ответ крайне меня опечалил, так как вскрыл некоторую нелогичность и корявость в целом стройного и красивого языка С++ в данном вопросе. Очевидно, что причины этого в сохранения в С++ обратной совместимости с С, но от этого не легче.

Итак, давайте разберемся по порядку (чтобы меня не обвинили в плагиате, сразу скажу, что я буду следовать примерному тексту Мейерса, так как он дал великолепное объяснение с примерами, и изобретать велосипед в данном случае было бы неразумно). Как мы предполагали, код std::istream_iterator<char>(is) создает экземпляр потокового итератора, привязанного к потоку is. И все бы ничего, если такая конструкция используется как самостоятельное объявление. Вся проблема в именно в использовании такого выражения в контексте вызова функции (в данном случае, конструктора), то есть в качестве параметра. Мейерс приводит следующий пример:

int f(double d);

Это команда объявления функции f, которая получает double и возвращает int.

Тоже самое происходит и в следующей строке. Круглые скобки вокруг имени параметра d не нужны, поэтому компилятор их игнорирует:

int f(double (d));     // То же; круглые скобки вокруг d игнорируются

Теперь третий вариант объявления той же функции. В нем имя параметра просто не указано:

int f(double);         // То же; имя параметра не указано

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

Теперь можно рассмотреть еще три объявления функции. В первом объявляется функция g с параметром — указателем на функцию, которая вызывается без параметров и возвращает double:

int g(double (*pf)()); // Функции g передается указатель на функцию

То же самое можно сформулировать иначе. Единственное различие заключается в том, что pf объявляется в синтаксисе без указателей (допустимом как в С, так и в С++):

int g(double pf());    // То же; pf неявно интерпретируется как указатель

Как обычно, имена параметров могут опускаться, поэтому возможен и третий вариант объявления g без указания имени pf:

int g(double());       // То же; имя параметра не указано

Обратите внимание на различия между круглыми скобками вокруг имени параметра (например, параметра d во втором объявлении f) и стоящими отдельно (как в этом примере). Круглые скобки, в которые заключено имя параметра, игнорируются, а стоящие отдельно, обозначают присутствие списка параметров; они сообщают о присутствии параметра, который является указателем на функцию.

Теперь вернемся к оригинальному примеру:

std::ifstream is("testfile.txt");
std::string val(
  std::istream_iterator<char>(is),
  std::istream_iterator<char>()
);

Сейчас я намеренно убрал таинственные “лишние” скобки вокруг первого параметра.

Что же перед нами тут? Совершенно не то, о чем мы думали изначально. Перед нами объявление функции val, возвращающей тип std::string. Функция получает два параметра:

  • Первый параметр, is, относится к типу istream_iterator<char>. Лишние круглые скобки вокруг is игнорируются.
  • Второй параметр не имеет имени. Он относится к типу указателя на функцию, которая вызывается без параметров и возвращает istream_iterator<char>.

А мы то тут ожидали увидеть описание вызова конструктора, которому передаются два потоковых итератора. Такая интерпретация написанного диктуется одним из основных правил C++: все, что может интерпретироваться как указатель функцию, должно интерпретироваться именно так. Так гласит стандарт:

В грамматике имеется неоднозначность, когда инструкция может быть выражением, так и объявлением. Если выражение с явным преобразованием типов в стиле вызова функции (expr.type.conv) является крайним слева, то оно может быть неотличимо от объявления, в котором первый оператор объявления начинается с открытой круглой скобки “(”. В этом случае инструкция рассматривается как объявление. — [C++03] п.6.8.

Так что же делают эти магические скобки вокруг первого параметра конструктора?

std::ifstream is("testfile.txt");
std::string val(
  (std::istream_iterator<char>(is)),
  std::istream_iterator<char>()
);

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

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

Как написал Герб Саттер в книге “Новые сложные задачи на С++” (он тоже посвятил этому вопросу целую главу, “Задача 23. Инициализация ли это?”, стр. 192, изд. “Вильямс”), что такие моменты синтаксиса С++ являются его “темными углами”, и их стоит избегать. Рассмотренный пример можно упростить, объявив итератор отдельно, а не прямо в тексте вызова конструктора, тем самым не заходить в “темный угол”. Не так элегантно, зато просто и понятно:

std::ifstream is("testfile.txt");
std::istream_iterator<char> begin(is);
std::istream_iterator<char> end;
std::string val(begin, end);

Читал я недавно, как Линус Торвальдс полоскал С++ за неоправданную языковую сложность. “C++ is a horrible language!”, — сказал Линус. Может он и прав.

Мыши плакали, кололись, но продолжали грызть С++.

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


Оригинальный пост | Disclaimer

Комментарии