Форум » C/C++ » Эволюция адаптеров контейнеров в новом стандарте С++ » Ответить

Эволюция адаптеров контейнеров в новом стандарте С++

Сыроежка: Сегодня занимался стандартным адаптером контейнеров std::stack, и у меня возник напрашивающийся вопрос: почему у адаптеров контейнеров в стандарте С++ 2003 нет члена функции swap? Ведь использовать общий стандартный алгоритм std::swap для адаптеров контейнеров вместо функций-членов swap, лежащих в их основе контейнеров, не эффективно. Сказано -сделано, и я решил реализовать функцию-член класса для адаптера контейнеров std::stack. Код выглядел следующим образом. Естественно, я свой код вводил в собственном пространстве имен usr::, чтобы не было коллизий со стандартным пространством имен std:: [pre2]namespace usr { template <typename T, typename Container = std::deque<T> > class stack { protected: Container c; ... public: void swap( stack<T, Container> &x ) { swap( c, x.c ); } ... }; template <typename T, typename Container> void swap( stack<T, Container> &x, stack<T, Container> &y { x.swap( y ); } } // end of namespace usr[/pre2] Далее в main я решил проверить, что получилось [pre2]int main() { typedef usr::stack<int>::container_type Container; const a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; usr::stack<int> s1( Container( a, a + sizeof( a ) / sizeof( *a ) ) ); usr::stack<int> s2; swap( s1, s2 ); return ( 0 ); } [/pre2] Но код упорно не желал компилироваться! Ошибка на самом деле не совсем очевидная. Дело в том, что при объявлении члена-функции swap в следствии алгоритма поиска имен в С++ получилось так, что вызов внутренней функции swap компилятор рассматривает как рекурсивный вызов этого же члена-функции и, естественно сообщает о том, что количество параметров слишком большее. То есть в этом коде вызов swap внутри тела функции-члена класса swap, воспринимается как рекурсивный вызов той же самой функции -члена класса. [pre2] void swap( stack<T, Container< &x ) { swap( c, x.c ); } [/pre2] Осознание этой ошибки отняло у меня достаточно время. Ошибка исправляется просто. Нужно было указать, что на этот раз вызывается функция swap из пространства имен std:: [pre2] void swap( stack<T, Container> &x ) { std::swap( c, x.c ); } [/pre2] Но самый главный вывод, который я сделал, состоял в том, что это было крайне странно и неестественно, что в стандарте С++ 2003 года для адаптеров контейнеров отсутствовала функция-член класса swap. На мой взгляд это очень серьезное и явное упущение! Из любопытства я решил заглянуть в новый стандарт С++ 2011. Неужели данную оплошность так и не исправили?! Нj к своему удовлетворению я увидел, что, наконец-то, функция-член класса swap в новом стандарте добавлена! Она выглядет следующим образом [pre2]void swap(stack& s) { using std::swap; swap(c, s.c); }[/pre2] Отличие от моей версии состоит в том, что здесь решили воспользоваться директивой using вместо явного указания имени пространства имен непосредственно перед вызовом функции swap, как это сделал я. Создается впечатление, что разработчики нового стандарта читают мои мысли!:)

Ответов - 3

Сыроежка: Почему на самом деле важна функция swap для адаптеров контейнеров? Дело не только в том, что может возникнуть потребность обменять содержимое двух адаптеров контейнеров. Проблема заключается еще в том, что если, например, в качестве базового контейнера для адаптера контейнера stack используется контейнер std::vector, то использование функции swap - это, практически, единственный эффективный способ "ужать" вектор. Известно, что векторы постоянно "растут", а метод resize не уменьшает емкость контейнера, а лишь позволяет ее увеличить. Поэтому для векторов используется следующий прием. [pre2]#include <iostream> #include <vector> int main() { const int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; std::vector<int> v( a, a + sizeof( a ) / sizeof( *a ) ); std::cout << "v.capacity() = " << v.capacity() << std::endl; // Мой компилятор выводит значение 10 v.push_back( 11 ); std::cout << "v.capacity() = " << v.capacity() << std::endl; // Здесь выводится значение 16 std::vector<int>( v ).swap( v ); std::cout << "v.capacity() = " << v.capacity() << std::endl; // Вектор ужат. Значение capacity = 11 return ( 0 ); } [/pre2] Главным в этом приеме сжатия вектора является вызов функции-члена класса swap для временного объекта типа std::vector, который создается путем копирования элементов из вектора, который нужно ужать. Затем содержимое временного вектора и исходного вектора обменирваются Этот прием описан в книге Герба Саттера "Решение сложных задач на С++". [pre2] std::vector<int>( v ).swap( v );[/pre2] Поэтому возникает естественный вопрос, а как ужать адаптер контейнера такой, как, например, стек, который в качестве базового контейнера использует вектор? Ведь у адаптеров контейнера согласно стандарту 2003 года функции-члена класса swap нет, и нет доступа к базовому контейнеру. Поэтому эта функция так важна. Разработчики компилятора MS VC++ 2010 предвосхитили выпуск нового стандарта С++ 2011 и заранее в свои адаптеры контейнеров включили функцию-член класса swap. Поэтому те, у кого есть этот компилятор, могут проверить ее действие у адаптеров контейнеров. Кроме того Microsoft также включили в этот класс функцию доступа к базовому класса _Get_container. Этой функции в стандарте С++ 2011 года нет, но можно ее воспользоваться для проверки работы функции -члена класса swap Итак, те, у кого есть MS VC++ 2010, могут выполнить следующий тест. [pre2]#include <iostream> #include <vector> #include <stack> int main() { typedef std::stack<int, std::vector<int> >::container_type Container; const int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; std::stack<int, vector<int> > s( Container( a, a + sizeof( a ) / sizeof( *a ) ) ); std::cout << "s._Get_container().capacity() = " << s._Get_container().capacity() << std::endl; // Мой компилятор выводит значение 10 s.push( 11 ); std::cout << "s._Get_container().capacity() = " << s._Get_container().capacity() << std::endl; // Здесь выводится значение 16 std::stack<int, vector<int> >( s ).swap( s ); std::cout << "s._Get_container().capacity() = " << s._Get_container().capacity() << std::endl; // Вектор ужат. Значение capacity = 11 return ( 0 ); } [/pre2] Думаю, что данное мое описание позволит помочь программистам ознакомиться с одним из многих изменений в стандарте С++, которые появились после выпуска новой редакции стандарта в 2011.

Сыроежка: К сожалению приходится константировать, что новый стандарт С++ также является некоторым суррогатом! То есть сделано лишь полшага в развитии адаптеров контейнеров. Для полного описания интерфейса, например, адаптера контейнера std::stack не хватает по крайней мере еще двух функций. Это функции, которая сообщает максимально допустимый размер стека [pre2]size_type max_size() const;[/pre2] и функции удаление элементов из стека [pre2]void clear();[/pre2] Отсутствие этих функций у адаптеров контейнеров - это серьезное упущение. Кроме того можно было бы ввести операторы ввода и вывода, как для потоков: << и >>.

Сыроежка: Еще один серьезный недостаток адаптера контейнеров std::stack заключается в отсутствии конструктора, принимающего список инициализации. Для простых задач было бы удобно инициализировать стек во время его объявления. Например, [pre2]std::stack<int> s = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };[/pre2] Однако, увы, этот код не будет компилироваться, так как соответствующего конструктора у класса std::stack нет. Можно достичь желаемого результата с помощью более сложной конструкции, учитывая то, что обычно лежащие в основе стека контейнеры имеют требуемый конструктор, принимающий в качестве параметра список инициализации. Вот как может выглядеть соответствующий код [pre2] #include <stack> #include <iostream> int main() { std::stack<int> s( std::deque<int>( { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 } ) ); while ( !s.empty() ) { std::cout << s.top() << ' '; s.pop(); } std::cout << std::endl; }[/pre2] Однако, согласитесь, такое объявление объекта класса std::stack выглядет более запутанным и менее наглядным по сравнению с предложением, [pre2]std::stack<int> s = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };[/pre2] если бы класс std::stack имел бы собственный конструктор, принимающий список инициализации. Конечно при этом лежащий в основе стека контейнер сам в свою очередь должен иметь такой конструктор, так как класс std::stack просто делегировал бы инициализацию конструктору этого контейнера. Я уже обратил внимание Комитета по стандартизации C++ на этот дефект в описании класса std::stack, который, кстати сказать, имеет место и у других адаптеров контейнеров, как, например, std::queue и std::priority_queue. Введение для адаптеров контейнеров конструктора , принимающего список инициализации, сделает интерфейс всех контейнеров более универсальным.




полная версия страницы