std::ifstream is("testfile.txt");
std::string val(
(std::istream_iterator<char>(is)),
std::istream_iterator<char>()
);
Обратите внимание на вроде "излишние" скобки вокруг первого параметра конструктора строки val
.
Скотт Мейерс в книге "Эффективное использование 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>
.
В грамматике имеется неоднозначность, когда инструкция может быть выражением, так и объявлением. Если выражение с явным преобразованием типов в стиле вызова функции (_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!", — сказал Линус. Может он и прав.Мыши плакали, кололись, но продолжали грызть С++.
Другие посты по теме:
Блестяще ! Спасибо за объяснение. Вот эта фраза была ключевой в понимании вопроса: "объявления формальных параметров не могут заключаться в круглые скобки, я вот заключить в круглые скобки аргумент при вызове функции можно"
ОтветитьУдалитьпо-моему очень даже в стиле С++ :) такой себе трик в нужную сторону (И попутно для сохранения стандарта).
P.S. это ж надо так любить тех, кто потом код читает, чтобы вместо отдельного объявления итератора засунуть его в вызов конструктора.