“Делайте простые вещи просто.” Лари Уолл
В C++ написание цикла, работающего с последовательностями - процесс нудный. Мы можем использовать либо итераторы, требующие немалого количества дополнительных действий, либо алгоритм std::for_each и перенести тело цикла в предикат, что не требует дополнительных действий, но заставляет перенести описание логики далеко от места, где она будет использована. В отличии от C++, некоторые языки, например Perl, имеют специальную конструкцию “foreach”, автоматизирующую этот процесс. BOOST_FOREACH как раз и есть такая конструкция для C++. Этот макрос перечисляет элементы последовательности, освобождая нас от необходимости иметь дело непосредственно с итераторами или описывать предикаты.
BOOST_FOREACH разработан для легкого и эффективного использования. Он не производит динамических выделений памяти, не вызывает виртуальные функции, не вызывает функции через указатели на них и не производит вызовов, непрозрачных для оптимизации компилятором. В результате происходит генерирование почти оптимального кода; быстродействие BOOST_FOREACH лишь на несколько процентов ниже быстродействия циклов, написанных «вручную». Кроме того, несмотря на то, что BOOST_FOREACH это макрос, он замечательно предсказуем в своем поведении. Он вычисляет свои аргументы только один раз, не привод к неприятным сюрпризам.
Ниже приведена программа, использующая BOOST_FOREACH для циклического перебора содержимого std::string.
#include <string> #include <iostream> #include <boost/foreach.hpp> int main() { std::string hello( "Hello, world!" ); BOOST_FOREACH( char ch, hello ) { std::cout << ch; } return 0; // Данная программа выводит следующее: // Hello, world! }
BOOST_FOREACH производит итерации последовательностей. Но что именно он “считает” последовательностью? Так как BOOST_FOREACH написан на основе Boost.Range, он автоматически поддерживает типы, которые Boost.Range воспринимает как последовательности. В частности, BOOST_FOREACH работает с типами, удовлетворяющими концепции «однопроходный диапазон». Так, BOOST_FOREACH может работать с:
Поддержка контейнеров STL очень широка, все что выглядит как STL контейнер может быть использовано. Если контейнер имеет встроенные типы iterator и const_iterator и члены-функции begin() и end(), BOOST_FOREACH автоматически может проводить вычисления с ним. Таким образом, boost::iterator_range<> и boost::sub_range<> работают с BOOST_FOREACH.
Для того, чтобы узнать, как заставить BOOST_FOREACH работать с другими типами смотри раздел Расширение.
Ниже приведены несколько примеров, показывающих все способы, которыми Вы можете использовать BOOST_FOREACH.
std::list<int> list_int( /*...*/ ); BOOST_FOREACH( int i, list_int ) { // do something with i }
short array_short[] = {1,2,3}; BOOST_FOREACH( int i, array_short ) { // short автоматически приводится к int }
std::deque<int> deque_int( /*...*/ ); int i = 0; BOOST_FOREACH( i, deque_int ) { if( i == 0 ) return; if( i == 1 ) continue; if( i == 2 ) break; }
элементов контейнера:
short array_short[] = { 1, 2, 3 }; BOOST_FOREACH( short & i, array_short ) { ++i; } // array_short теперь содержит {2,3,4}
std::vector<std::vector<int> > matrix_int; BOOST_FOREACH( std::vector<int> & row, matrix_int ) BOOST_FOREACH( int & i, row ) ++i;
extern std::vector<float> get_vector_float(); BOOST_FOREACH( float f, get_vector_float() ) { // Замечание: get_vector_float() вызывается только один раз }
Перечисление элементов в последовательности, заданной rvalue не работает на некоторых старых компиляторах. Список компиляторов поддерживающих такую возможность см. в разделе Переносимость.
Некоторым людям не нравится название BOOST_FOREACH. Оно слишком длинное. ВСЕ ЗАГЛАВНЫЕ БУКВЫ утомительны для чтения. Возможно, эти претензии справедливы, но BOOST_FOREACH просто следует правилам именования Boost. Хотя, это и не значит, что Вас это должно озадачивать. Если Вы хотите использовать другой идентификатор (например, foreach), Вы можете просто написать:
#define foreach BOOST_FOREACH
Поступайте так, только если вы уверены, что идентификатор, который Вы выбрали, не приведет к конфликту имен в вашем коде. ПРИМЕЧАНИЕ Не используйте #define foreach(x,y) BOOST_FOREACH(x,y). Это может привести к проблемам, если аргументы сами являются макросами. Это может привести к дополнительному разворачиванию этих макросов. Вместо этого используйте форму, описанную выше.
Если мы хотим использовать BOOST_FOREACH для перечисления элементов некоторого нового типа контейнера, мы должны «научить» BOOST_FOREACH работать с нашим типом. Так как BOOST_FOREACH реализован с помощью Boost.Range, мы должны расширить Boost.Range для расширения BOOST_FOREACH. Данный раздел детально описывает расширение Boost:Range.
Ниже приведен пример расширения BOOST_FOREACH для перечисления элементов в типе sub_string, который содержит два итератора std::string.
namespace my { // sub_string: часть строки, ограниченной парой итераторов struct sub_string { std::string::iterator begin; std::string::iterator end; /* ... реализация ... */ }; // Добавить перезагрузку boost_range_begin() и boost_range_end() в пространстве имен, // в котором расположена sub_string, для того чтобы быть обнаруженным Argument-Dependent Lookup. inline std::string::iterator boost_range_begin( sub_string & x ) { return x.begin; } inline std::string::iterator boost_range_end( sub_string & x ) { return x.end; } // Так же перегрузим const sub_strings. Обратите внимание что используется // преобразование из string::iterator в string::const_iterator. inline std::string::const_iterator boost_range_begin( sub_string const & x ) { return x.begin; } inline std::string::const_iterator boost_range_end( sub_string const & x ) { return x.end; } } namespace boost { // определение rannge_iterator и range_const_iterator в пространстве имен boost template<> struct range_iterator< my::sub_string > { typedef std::string::iterator type; }; template<> struct range_const_iterator< my::sub_string > { typedef std::string::const_iterator type; }; }
Теперь, когда мы научили Boost.Range (и, соответственно, BOOST_FOREACH) работе с нашим типом, мы можем использовать BOOST_FOREACH для перечисления элементов нашего типа sub_string.
my::sub_string substr; BOOST_FOREACH( char ch, substr ) { // Ура! }
Существует несколько проблем с переносимостью, которых мы должны избежать при расширении BOOST_FOREACH. Проверьте раздел переносимость. В частности, если ваш компилятор не поддерживает Argument-Dependent Lookup, раздел «Переносимость» для Boost.Range, предлагающий рекомендуемые решения.
Для работы с non-copyable типами-последовательностями, нам необходимо указать BOOST_FOREACH, что он не должен пытаться делать копий. Если ваш тип наследуется от boost::noncopyable, то никаких действий предпринимать не надо. Если же нет, необходимо конкретизировать boost::foreach::is_noncopyable<> шаблон, как показано ниже:
class noncopy_vector { // ... private: noncopy_vector( noncopy_vector const & ); // non-copyable! }; namespace boost { namespace foreach { template<> struct is_noncopyable< noncopy_vector > : mpl::true_ { }; }}
Другой способ добиться того же эффекта - перегрузить глобальнуюфункцию boost_foreach_is_noncopyable(). Преимущество этого способа в том, что его применение сохраняет совместимость со старыми компиляиорами.
// В глобальном нэймспейсе... inline boost::mpl::true_ * boost_foreach_is_noncopyable( noncopy_vector *&, boost::foreach::tag ) { return 0; }
Примечание. Хотя мы и должны указывать BOOST_FOREACH, что наш тип non-copyable, это вовсе не означает, что BOOST_FOREACH всегда создоет копии нашего типа. Очевидно, что такое поведение непроизводительно, а в некоторых случаях ведет к ошибкам. BOOST_FOREACH достаточно «умен», чтобы поять, когда создавать копии, а когда нет. Признак is_noncopyable<> необходим, чтобы избежать копирования в случае когда оно невозможно.
На некоторых компиляторах BOOST_FOREACH должен иногда генерировать немного медленный код, чтобы гарантировать корректную обработку последовательностей во временно создаваемых объектах. Он как бы спрашивает себя «Должен ли я делать копию этого объекта» и затем, «Сделал ли я копию или нет?». Для некоторых типов это черезмерно. Представьте себе последовательность, являющуюся простой парой итераторов. Прыжки через горящий обруч(танцы с бубном)для избежания копирования бессмысленны, потому что копирование дешево.
Пара итераторов это пример легковесной оболочки. Она не хранит значения в последовательности, а всего лишь их итераторы. Это значит, что перечисление в копии легковесной оболочки даст те же результаты, что и перечисление с использованием объекта. Для таких типов BOOST_FOREACH предлагает средство, позволяющее нам не беспокоиться о расходах на копирование. Это может привести к более быстрому исполнению цикла. Просто конкретизируйте boost::foreach::is_lightweight_proxy<> свойство как указано ниже:
struct sub_string : boost::iterator_range< std::string::iterator > { // ... }; namespace boost { namespace foreach { template<> struct is_lightweight_proxy< sub_string > : mpl::true_ { }; }}
Того же эффекта можно добиться перегрузкой глобальной функции, как указано ниже:
// В глобальном пространстве имен... inline boost::mpl::true_ * boost_foreach_is_lightweight_proxy( sub_string *&, boost::foreach::tag ) { return 0; }
Этот метод совместим со старыми компиляторами.
BOOST_FOREACH использует некоторые передовые техники, не поддерживаемые всеми компиляторами. В зависимости от того, как на сегодняшний день Ваш компилятор совместим с Boost, Вы не можете испольовать BOOST_FOREACH в некоторых сценариях. Так как BOOST_FOREACH использует Boost.Range, он наследует принципы переносимости этой библиотеки. Вы можете прочитать об этом в разделе Boost.Range:Переносимость.
В дополнение к требовниям к компилятору Boost.Range, BOOST_FOREACH выдвигает собственные требования для правильной обработки последовательностей rvalue. (Помните, что rvalue это неименнованный объект, таким образом последовательность rvalue может быть функцией, возвращающей std::vector<> по значению.) Компиляторы по-разному обрабатывают rvalues и lvalues. Для охвата всех типов компиляторов BOOST_FOREACH определяет три типа совместимости, описанных в таблице:
Уровни совместимости BOOST_FOREACH | |
---|---|
Уровень | Значение |
Уровень 0 | Высший уровень совместимости. BOOST_FOREACH работает с lvalues, rvalues и const-qualified rvalues. |
Уровень 1 | Средний уровень совместимости. BOOST_FOREACH работает с lvalues and обычными rvalues, но не с const-qualified rvalues. В этом случае определено BOOST_FOREACH_NO_CONST_RVALUE_DETECTION. |
Уровень 2 | Низший уровень совместимости. BOOST_FOREACH работает только с lvalues. В этом случае определено BOOST_FOREACH_NO_RVALUE_DETECTION. |
Ниже приведен список компиляторов, на которых тестировался BOOST_FOREACH и и уровень их совместимости с BOOST_FOREACH.
Compiler | Compliance Level |
---|---|
Visual C++ 8.0 | Level 0 |
Visual C++ 7.1 | Level 0 |
Visual C++ 7.0 | Level 2 |
Visual C++ 6.0 | Level 2 |
gcc 4.0 | Level 0 |
gcc 3.4 | Level 0 |
gcc 3.3 | Level 0 |
mingw 3.4 | Level 0 |
Intel for Linux 9.0 | Level 0 |
Intel for Windows 9.0 | Level 0 |
Intel for Windows 8.0 | Level 1 |
Intel for Windows 7.0 | Level 2 |
Comeau 4.3.3 | Level 0 |
Borland 5.6.4 | Level 2 |
Metrowerks 9.5 | Level 1 |
Metrowerks 9.4 | Level 1 |
SunPro 5.8 | Level 2 |
qcc 3.3 | Level 0 |
tru64cxx 65 | Level 2 |
tru64cxx 71 | Level 2 |
Идеи BOOST_FOREACH появились в Microsoft Visual C++ Group на этапе раннего проектирования C++/CLI. Вопрос о добавлении к языку конструкции «foreach» до сих пор открыт. В качестве мысленного эксперимента Энсон Цао (Anson Tsao) разослал пробный код, демонстрирующий, возможность такого решения методами стандартной библиотеки. Этот код был написан на современом тогда диалекте C++/CLI, для которого тогда еще не было компилятора. Я был заинтригован такой возможностью и портировал его код на Managed C++ и довел его до рабочего сотояния. Мы работали вместе для улучшения этой идеи и даже опубликовали статью о ней в ноябрьском выпуске 2003 года CUJ.
После того, как я ушел из Microsoft, я переписал макрос на стандартном C++, исправил некоторые изъяны версии CUJ, и окрестил его BOOST_FOREACH. В октябре 2003 я начал дискуссию о BOOST_FOREACH в Boost developers list, где он был встречен достаточно равнодушно. Я оставил эту задачу до декабря 2004, когда я снова переписал BOOST_FOREACH. Новая версия вычисляла выражение, задающее последовательность, только один раз и правильно обрабатывала lvalue и rvalue выражения, задающие последовательность. Она была написана на основе только что принятой библиотеки Boost.Range с улучшенной переносимостью. Эту версию 12 декабря 2004 я наконец-таки выложил для обозрения. Она была включена в состав Boost 5 мая 2005.
Спасибо Энсону Цао (Anson Tsao) из Microsoft за то, что он натолкнул меня на этк идею и продемонстрировал возможность ее осуществления. Хотелось бы также поблагодарить Торстена Оттосена (Thorsten Ottosen) за библиотеку Boost.Range на основе которой написана текущая версия BOOST_FOREACH. И, наконец, хотелось бы поблагодарить Рассела Хинда (Russell Hind), Элисдэйр Мередит (Alisdair Meredith) и Стивена Слапета (Stefan Slapeta) за их помощь в портировании для различных компиляторов.
Для получения более подробной информации о том, как работает BOOST_FOREACH, Вы можете прочесть статью Conditional Love в The C++ Source.