Я нашел таки для себя ответ на вопрос про “лишние скобки” вокруг параметра, задающего интервальный итератор (см. “Скоростное чтение файла в 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!”, — сказал Линус. Может он и прав.
Мыши плакали, кололись, но продолжали грызть С++.
Другие посты по теме: