*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***

понедельник, 9 февраля 2009 г.

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

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


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

1 комментарий:

  1. Блестяще ! Спасибо за объяснение. Вот эта фраза была ключевой в понимании вопроса: "объявления формальных параметров не могут заключаться в круглые скобки, я вот заключить в круглые скобки аргумент при вызове функции можно"

    по-моему очень даже в стиле С++ :) такой себе трик в нужную сторону (И попутно для сохранения стандарта).

    P.S. это ж надо так любить тех, кто потом код читает, чтобы вместо отдельного объявления итератора засунуть его в вызов конструктора.

    ОтветитьУдалить