![]() |
![]() ![]() ![]() ![]() |
![]() |
Iterator FacadeBoost , ,
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Author: | Дэвид Абрахамс, Джереми Сик, Томас Витт | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Contact: | В то время как интерфейс итератора богат, есть базовое подмножество интерфейса, которое необходимо для всей функциональности. Мы определили следующие основные модели поведения итераторов:
В дополнение к поведению, перечисленному выше, основные элементы интерфейса включают связанные типы, подверженные через черты итератора:значение_тип,ссылка,Различие_типиитератор_категория. Фасад итератора использует Curiously Recurring Template Pattern (CRTP)[Cop95], чтобы пользователь мог указать поведениеiterator_facadeв производном классе. Бывшие проекты использовали объекты политики для определения поведения, но этот подход был отброшен по нескольким причинам:
UsageПользовательiterator_facadeполучает свой класс итератора из специализацииiterator_facadeи проходит класс производного итератора какiterator_facadeпервый параметр шаблона. Порядок других параметров шаблона был тщательно выбран, чтобы воспользоваться полезными по умолчанию. Например, при определении постоянного итератора lvalue пользователь может передать конст-квалифицированную версию параметраvalue_typeитератораiterator_facade'sValueи опустить параметр.Ссылкапараметр, который следует. Полученный класс итератора должен определять функции члена, реализующие основные модели поведения итератора. В следующей таблице описаны выражения, которые должны быть действительными в зависимости от категории производного типа итератора. Эти функции элементов кратко описаны ниже и более подробно в требованиях к фасаду итератора.
В дополнение к реализации основных функций интерфейса итератор, полученный изiterator_facade, обычно определяет несколько конструкторов. Чтобы смоделировать любую из стандартных концепций итератора, итератор должен иметь конструктор копий. Кроме того, если тип итератораXпредназначен для автоматического взаимодействия с другим типом итератораY(как с постоянными и изменяемыми итераторами), то должно быть неявное преобразование изXвYили изYвX(но не оба), обычно реализованное в качестве конструктора преобразования. Наконец, если итератор предназначен для моделирования итератора переднего поворота или более усовершенствованной концепции итератора, требуется конструктор по умолчанию. Iterator Core Accessiterator_facadeи реализации оператора должны иметь возможность доступа к основным функциям-членам в производном классе. Публикация основных функций-членов раскрывает пользователю детали реализации. Используемый здесь дизайн гарантирует, что детали реализации не отображаются в публичном интерфейсе производного типа итератора. Предотвращение прямого доступа к основным функциям имеет два преимущества. Во-первых, у пользователя нет возможности случайно использовать функцию члена итератора, когда был предназначен элемент типа значения. В прошлом это было проблемой с интеллектуальными реализациями указателей. Второе и главное преимущество заключается в том, что реализаторы библиотеки могут свободно обмениваться ручной реализацией итератора на реализацию на основеiterator_facade, не опасаясь взлома кода, который имел доступ к функциям публичного ядра. В наивной реализации сохранение в тайне функций основного члена производного класса потребовало бы от него предоставления дружбыiterator_facadeи каждому из семи операторов. Для того чтобы уменьшить бремя ограничения доступа, предоставляетсяiterator_core_access, класс, который действует как шлюз к функциям основного члена в классе производного итератора. Автору производного класса нужно только предоставить дружбуiterator_core_access, чтобы его основные функции члена были доступны библиотеке. iterator_core_accessбудет, как правило, реализован в виде пустого класса, содержащего только частные статические функции члена, которые вызывают функции основного члена итератора. Однако нет необходимости стандартизировать протокол шлюза. Обратите внимание, что даже еслиитератор_core_accessиспользует публичные функции-члены, это не откроет лазейку безопасности, поскольку каждая функция-ядро сохраняет инварианты итератора. operator[]Оператор индексации для обобщенного итератора представляет особые проблемы. Оператор итератора случайного доступадолжен только вернуть что-то конвертируемое в егозначение_тип. Требование, чтобы он возвращал значение l, исключало бы в настоящее время законные итераторы случайного доступа, которые содержат указанное значение в элементе данных (например,подсчёт_iterator), потому что* (p+n)является ссылкой на временный итераторp+n, который разрушается, когдаоператор []возвращается. Письменные итераторы, построенные с помощьюiterator_facade, реализуют семантику, требуемую предпочтительным разрешениемвыпуска 299и принятую предложениемn1550: результатp[n]является объектом, конвертируемым в значение итератора[типа], иp[n]xэквивалентен* [p+n]=x(Примечание: Этот объект результата может быть реализован в качестве прокси, содержащего копиюp+n. Этот подход будет работать должным образом для любого итератора случайного доступа независимо от других деталей его реализации. Пользователь, который знает больше о реализации своего итератора, может свободно реализовать оператора, который возвращает значение l в классе производного итератора; он будет скрывать тот, который поставляетсяiterator_facadeот клиентов своего итератора. operator->Тип ссылкичитаемого итератора (и сегодняшнего итератора ввода) на самом деле не должен быть ссылкой, если он конвертируем в значениеитератора. Однако, когдазначение_типявляется классом, все равно должна быть возможность доступа к членам черезоператора->. Поэтому итератор, чейэталонныйтип фактически не является эталонным, должен возвращать прокси-сервер, содержащий копию указанной величины от егооператора->. Типы возврата дляитератора_фасада'sоператора->иоператора[]явно не указаны. Вместо этого эти типы описаны с точки зрения набора требований, которые должны быть удовлетворены реализациейiterator_facade.
Reference
template <
class Derived
, class Value
, class CategoryOrTraversal
, class Reference = Value&
, class Difference = ptrdiff_t
>
class iterator_facade {
public:
typedef remove_const<Value>::type value_type;
typedef Reference reference;
typedef Value* pointer;
typedef Difference difference_type;
typedef /* see below */ iterator_category;
reference operator*() const;
/* see below */ operator->() const;
/* see below */ operator[](difference_type n) const;
Derived& operator++();
Derived operator++(int);
Derived& operator--();
Derived operator--(int);
Derived& operator+=(difference_type n);
Derived& operator-=(difference_type n);
Derived operator-(difference_type n) const;
protected:
typedef iterator_facade iterator_facade_;
};
// Comparison operators
template <class Dr1, class V1, class TC1, class R1, class D1,
class Dr2, class V2, class TC2, class R2, class D2>
typename enable_if_interoperable<Dr1,Dr2,bool>::type // exposition
operator ==(iterator_facade<Dr1,V1,TC1,R1,D1> const& lhs,
iterator_facade<Dr2,V2,TC2,R2,D2> const& rhs);
template <class Dr1, class V1, class TC1, class R1, class D1,
class Dr2, class V2, class TC2, class R2, class D2>
typename enable_if_interoperable<Dr1,Dr2,bool>::type
operator !=(iterator_facade<Dr1,V1,TC1,R1,D1> const& lhs,
iterator_facade<Dr2,V2,TC2,R2,D2> const& rhs);
template <class Dr1, class V1, class TC1, class R1, class D1,
class Dr2, class V2, class TC2, class R2, class D2>
typename enable_if_interoperable<Dr1,Dr2,bool>::type
operator <(iterator_facade<Dr1,V1,TC1,R1,D1> const& lhs,
iterator_facade<Dr2,V2,TC2,R2,D2> const& rhs);
template <class Dr1, class V1, class TC1, class R1, class D1,
class Dr2, class V2, class TC2, class R2, class D2>
typename enable_if_interoperable<Dr1,Dr2,bool>::type
operator <=(iterator_facade<Dr1,V1,TC1,R1,D1> const& lhs,
iterator_facade<Dr2,V2,TC2,R2,D2> const& rhs);
template <class Dr1, class V1, class TC1, class R1, class D1,
class Dr2, class V2, class TC2, class R2, class D2>
typename enable_if_interoperable<Dr1,Dr2,bool>::type
operator >(iterator_facade<Dr1,V1,TC1,R1,D1> const& lhs,
iterator_facade<Dr2,V2,TC2,R2,D2> const& rhs);
template <class Dr1, class V1, class TC1, class R1, class D1,
class Dr2, class V2, class TC2, class R2, class D2>
typename enable_if_interoperable<Dr1,Dr2,bool>::type
operator >=(iterator_facade<Dr1,V1,TC1,R1,D1> const& lhs,
iterator_facade<Dr2,V2,TC2,R2,D2> const& rhs);
// Iterator difference
template <class Dr1, class V1, class TC1, class R1, class D1,
class Dr2, class V2, class TC2, class R2, class D2>
/* see below */
operator-(iterator_facade<Dr1,V1,TC1,R1,D1> const& lhs,
iterator_facade<Dr2,V2,TC2,R2,D2> const& rhs);
// Iterator addition
template <class Dr, class V, class TC, class R, class D>
Derived operator+ (iterator_facade<Dr,V,TC,R,D> const&,
typename Derived::difference_type n);
template <class Dr, class V, class TC, class R, class D>
Derived operator+ (typename Derived::difference_type n,
iterator_facade<Dr,V,TC,R,D> const&);
The iterator_category member of iterator_facade is iterator-category(CategoryOrTraversal, value_type, reference) гдеитератор-категорияопределена следующим образом:
iterator-category(C,R,V) :=
if (C is convertible to std::input_iterator_tag
|| C is convertible to std::output_iterator_tag
)
return C
else if (C is not convertible to incrementable_traversal_tag)
the program is ill-formed
else return a type X satisfying the following two constraints:
1. X is convertible to X1, and not to any more-derived
type, where X1 is defined by:
if (R is a reference type
&& C is convertible to forward_traversal_tag)
{
if (C is convertible to random_access_traversal_tag)
X1 = random_access_iterator_tag
else if (C is convertible to bidirectional_traversal_tag)
X1 = bidirectional_iterator_tag
else
X1 = forward_iterator_tag
}
else
{
if (C is convertible to single_pass_traversal_tag
&& R is convertible to V)
X1 = input_iterator_tag
else
X1 = C
}
2. category-to-traversal(X) is convertible to the most
derived traversal tag type to which X is also
convertible, and not to any more-derived traversal tag
type.
[Примечание: намерение состоит в том, чтобы позволитьiterator_categoryбыть одним из пяти исходных тегов категории, когда конвертируемость в один из тегов обхода не добавит информации] Шаблонenable_if_interoperable, используемый выше, предназначен для экспозиции. Операторы-члены должны находиться только в наборе перегрузок при условии, что производные типыDr1иDr2совместимы, что означает, что по меньшей мере один из типов является конвертируемым в другой. Подходпозволяет_if_interoperableиспользовать SFINAE для вывода операторов из набора перегрузок, когда типы не совместимы. Операторы должны вести себякак-есливключить_if_интероперабельностьбыло определено, что:
template <bool, typename> enable_if_interoperable_impl
{};
template <typename T> enable_if_interoperable_impl<true,T>
{ typedef T type; };
template<typename Dr1, typename Dr2, typename T>
struct enable_if_interoperable
: enable_if_interoperable_impl<
is_convertible<Dr1,Dr2>::value || is_convertible<Dr2,Dr1>::value
, T
>
{};
iterator_facade RequirementsВ следующей таблице описаны типичные действительные выражения поiterator_facade'sПроизводныйпараметр, в зависимости от концепции(ей) итератора, которую он будет моделировать. Операции в первой колонке должны быть доступны для членских функций классаiterator_core_access. Кроме того,static_cast В приведенной ниже таблицеFявляетсяитератором_фасада iterator_facade Core Operations
iterator_facade operationsОперации в этом разделе описаны в терминах операций на базовом интерфейсеПроизводный, который может быть недоступным (т.е. закрытым). Реализация должна обеспечивать доступ к этим операциям через функции-члены классаiterator_core_access. ссылкаоператор*()const;
оператор->()const;(см.ниже)
unspecified operator[](difference_type n) const;
Derived&operator++();
Выводитсяоператор++(int);
Производный &оператор--();
Производныйоператор - (int);
Производный &оператор +=(разница_типn);
Derived&operator-=(difference_typen);
Выводитсяоператор-(различие_типn)const;
template <class Dr, class V, class TC, class R, class D>
Derived operator+ (iterator_facade<Dr,V,TC,R,D> const&,
typename Derived::difference_type n);
template <class Dr, class V, class TC, class R, class D>
Derived operator+ (typename Derived::difference_type n,
iterator_facade<Dr,V,TC,R,D> const&);
template <class Dr1, class V1, class TC1, class R1, class D1,
class Dr2, class V2, class TC2, class R2, class D2>
typename enable_if_interoperable<Dr1,Dr2,bool>::type
operator ==(iterator_facade<Dr1,V1,TC1,R1,D1> const& lhs,
iterator_facade<Dr2,V2,TC2,R2,D2> const& rhs);
template <class Dr1, class V1, class TC1, class R1, class D1,
class Dr2, class V2, class TC2, class R2, class D2>
typename enable_if_interoperable<Dr1,Dr2,bool>::type
operator !=(iterator_facade<Dr1,V1,TC1,R1,D1> const& lhs,
iterator_facade<Dr2,V2,TC2,R2,D2> const& rhs);
template <class Dr1, class V1, class TC1, class R1, class D1,
class Dr2, class V2, class TC2, class R2, class D2>
typename enable_if_interoperable<Dr1,Dr2,bool>::type
operator <(iterator_facade<Dr1,V1,TC1,R1,D1> const& lhs,
iterator_facade<Dr2,V2,TC2,R2,D2> const& rhs);
template <class Dr1, class V1, class TC1, class R1, class D1,
class Dr2, class V2, class TC2, class R2, class D2>
typename enable_if_interoperable<Dr1,Dr2,bool>::type
operator <=(iterator_facade<Dr1,V1,TC1,R1,D1> const& lhs,
iterator_facade<Dr2,V2,TC2,R2,D2> const& rhs);
template <class Dr1, class V1, class TC1, class R1, class D1,
class Dr2, class V2, class TC2, class R2, class D2>
typename enable_if_interoperable<Dr1,Dr2,bool>::type
operator >(iterator_facade<Dr1,V1,TC1,R1,D1> const& lhs,
iterator_facade<Dr2,V2,TC2,R2,D2> const& rhs);
template <class Dr1, class V1, class TC1, class R1, class D1,
class Dr2, class V2, class TC2, class R2, class D2>
typename enable_if_interoperable<Dr1,Dr2,bool>::type
operator >=(iterator_facade<Dr1,V1,TC1,R1,D1> const& lhs,
iterator_facade<Dr2,V2,TC2,R2,D2> const& rhs);
template <class Dr1, class V1, class TC1, class R1, class D1,
class Dr2, class V2, class TC2, class R2, class D2>
typename enable_if_interoperable<Dr1,Dr2,difference>::type
operator -(iterator_facade<Dr1,V1,TC1,R1,D1> const& lhs,
iterator_facade<Dr2,V2,TC2,R2,D2> const& rhs);
Tutorial ExampleВ этом разделе мы рассмотрим реализацию нескольких итераторов с использованиемiterator_facade, основанных на простом примере связанного списка полиморфных объектов. Этот пример был вдохновленпостингомКита Макдональда всписке рассылки Boost-Users. The ProblemСкажем, мы написали полиморфный связанный список узлов базового класса:
# include <iostream>
struct node_base
{
node_base() : m_next(0) {}
// Each node manages all of its tail nodes
virtual ~node_base() { delete m_next; }
// Access the rest of the list
node_base* next() const { return m_next; }
// print to the stream
virtual void print(std::ostream& s) const = 0;
// double the value
virtual void double_me() = 0;
void append(node_base* p)
{
if (m_next)
m_next->append(p);
else
m_next = p;
}
private:
node_base* m_next;
};
Списки могут содержать объекты разных типов, связывая между собой специализации следующего шаблона:
template <class T>
struct node : node_base
{
node(T x)
: m_value(x)
{}
void print(std::ostream& s) const { s << this->m_value; }
void double_me() { m_value += m_value; }
private:
T m_value;
};
И мы можем распечатать любой узел, используя следующего оператора потоковой передачи:
inline std::ostream& operator<<(std::ostream& s, node_base const& n)
{
n.print(s);
return s;
}
Наша первая задача - создать соответствующий итератор по этим спискам. A Basic Iterator Using iterator_facadeМы построим классnode_iterator, используя наследование изiterator_facadeдля реализации большинства операций итератора.
# include "node.hpp"
# include <boost/iterator/iterator_facade.hpp>
class node_iterator
: public boost::iterator_facade<...>
{
...
};
Template Arguments for iterator_facadeiterator_facadeимеет несколько параметров шаблона, поэтому мы должны решить, какие типы использовать для аргументов. Параметры:Получено,Значение,КатегорияПутешественник,СсылкаиРазница. DerivedПосколькуiterator_facadeпредназначен для использования с CRTP[Cop95], первым параметром является само название класса итератораnode_iterator. ValueПараметрЗначениеопределяетnode_iterator'sзначение_type. В этом случае мы повторяем надnode_baseобъектами, поэтомузначениебудетnode_base. CategoryOrTraversalТеперь мы должны определить, какаяконцепция обхода итераторанашаузел_итераторсобирается моделировать. Связанные с помощью Singly списки имеют только прямые ссылки, поэтому наш итератор не может бытьдвунаправленным итератором обхода. Наш итератор должен иметь возможность совершать несколько проходов по одному и тому же связанному списку (в отличие, скажем, отistream_iterator, который потребляет поток, по которому он проходит), поэтому он должен бытьпередним итератором обхода. Поэтому мы пройдембустер::forward_traversal_tagв этом положении1.
ReferenceАргументReferenceстановится типом, возвращаемымnode_iterator's dereference operation, а также будет таким же, какstd::iterator_traits DifferenceАргументРазницаопределяет, как будет измеряться расстояние между двумяnode_iterators, а также будет таким же, какstd::iterator_traits Декларацияnode_iteratorбудет выглядеть примерно так:
# include "node.hpp"
# include <boost/iterator/iterator_facade.hpp>
class node_iterator
: public boost::iterator_facade<
node_iterator
, node_base
, boost::forward_traversal_tag
>
{
...
};
Constructors and Data MembersДалее нужно решить, как представлять позицию итератора. Это представление будет принимать форму членов данных, поэтому нам также нужно будет написать конструкторы для их инициализации. Положениеnode_iteratorвполне естественно представлено с помощью указателя наnode_base. Нам понадобится конструктор для создания итератора изnode_base*, и конструктор по умолчанию для удовлетворения требованийforward traversal iterator2.
# include "node.hpp"
# include <boost/iterator/iterator_facade.hpp>
class node_iterator
: public boost::iterator_facade<
node_iterator
, node_base
, boost::forward_traversal_tag
>
{
public:
node_iterator()
: m_node(0)
{}
explicit node_iterator(node_base* p)
: m_node(p)
{}
private:
...
node_base* m_node;
};
Implementing the Core OperationsПоследним шагом является реализация основных операций, требуемых концепциями, которые мы хотим, чтобы наш итератор моделировал. Ссылаясь на таблицу, мы видим, что первые три строки применимы, потому чтоузел_итератордолжен удовлетворять требованиям кчитаемому итератору,одиночному итераторуиинкрементируемому итератору. Поэтому мы должны предоставить,равныеиприращениячленов. Мы не хотим, чтобы эти участники стали частью публичного интерфейсаnode_iterator, поэтому мы можем сделать их частными и предоставить дружбу, чтобы повысить::iterator_core_access, "back-door", которыйiterator_facadeиспользует для получения доступа к основным операциям:
# include "node.hpp"
# include <boost/iterator/iterator_facade.hpp>
class node_iterator
: public boost::iterator_facade<
node_iterator
, node_base
, boost::forward_traversal_tag
>
{
public:
node_iterator()
: m_node(0) {}
explicit node_iterator(node_base* p)
: m_node(p) {}
private:
friend class boost::iterator_core_access;
void increment() { m_node = m_node->next(); }
bool equal(node_iterator const& other) const
{
return this->m_node == other.m_node;
}
node_base& dereference() const { return *m_node; }
node_base* m_node;
};
Voilà; полный и соответствующий читаемый, передний итератор! Для рабочего примера его использования см.эту программу. A constant node_iteratorТеперь нашnode_iteratorпредоставляет клиентам доступ как кnode'sprint [std::ostream&]constчленской функции, так и к его мутирующемуdouble_me()члену. Если бы мы хотели построить константуnode_iterator, нам пришлось бы внести только три изменения:
class const_node_iterator
: public boost::iterator_facade<
const_node_iterator
, node_base const
, boost::forward_traversal_tag
>
{
public:
const_node_iterator()
: m_node(0) {}
explicit const_node_iterator(node_base* p)
: m_node(p) {}
private:
friend class boost::iterator_core_access;
void increment() { m_node = m_node->next(); }
bool equal(const_node_iterator const& other) const
{
return this->m_node == other.m_node;
}
node_base const& dereference() const { return *m_node; }
node_base const* m_node;
};
На самом делеnode_iteratorиconst_node_iteratorнастолько похожи, что имеет смысл разложить общий код на шаблон следующим образом:
template <class Value>
class node_iter
: public boost::iterator_facade<
node_iter<Value>
, Value
, boost::forward_traversal_tag
>
{
public:
node_iter()
: m_node(0) {}
explicit node_iter(Value* p)
: m_node(p) {}
private:
friend class boost::iterator_core_access;
bool equal(node_iter<Value> const& other) const
{
return this->m_node == other.m_node;
}
void increment()
{ m_node = m_node->next(); }
Value& dereference() const
{ return *m_node; }
Value* m_node;
};
typedef node_iter<node_base> node_iterator;
typedef node_iter<node_base const> node_const_iterator;
InteroperabilityНашconst_node_iteratorотлично работает сам по себе, но вместе сnode_iteratorон не совсем соответствует ожиданиям. Например, мы хотели бы иметь возможность пройтиnode_iterator, где ожидалосьnode_const_iterator, так же, как вы можете сstd::list Эта ожидаемая способность использовать два различных типа итераторов вместе известна каксовместимость. Достижение функциональной совместимости в нашем случае так же просто, как и уравнение.функция и добавление шаблонного преобразующего конструктора34:
template <class Value>
class node_iter
: public boost::iterator_facade<
node_iter<Value>
, Value
, boost::forward_traversal_tag
>
{
public:
node_iter()
: m_node(0) {}
explicit node_iter(Value* p)
: m_node(p) {}
template <class OtherValue>
node_iter(node_iter<OtherValue> const& other)
: m_node(other.m_node) {}
private:
friend class boost::iterator_core_access;
template <class> friend class node_iter;
template <class OtherValue>
bool equal(node_iter<OtherValue> const& other) const
{
return this->m_node == other.m_node;
}
void increment()
{ m_node = m_node->next(); }
Value& dereference() const
{ return *m_node; }
Value* m_node;
};
typedef impl::node_iterator<node_base> node_iterator;
typedef impl::node_iterator<node_base const> node_const_iterator;
Вы можете увидеть пример программы, которая выполняет наши совместимые итераторыздесь. Telling the TruthТеперьnode_iteratorиnode_const_iteratorведут себя точно так, как вы ожидаете... почти. Мы можем сравнить их и преобразовать в одном направлении: отnode_iteratorдоnode_const_iterator. Если мы попытаемся преобразовать изnode_const_iteratorвnode_iterator, мы получим ошибку, когда преобразующий конструктор попытается инициализироватьnode_iterator'sm_node, узелс узеломconst*. Так в чем проблема? Проблема в том, чтоboost:: На самом деле, этот вид магии возможен с помощьюповышения::enable_if. Переписывая преобразующий конструктор следующим образом, мы можем удалить его из набора перегрузок, когда это не подходит:
#include <boost/type_traits/is_convertible.hpp>
#include <boost/utility/enable_if.hpp>
...
private:
struct enabler {};
public:
template <class OtherValue>
node_iter(
node_iter<OtherValue> const& other
, typename boost::enable_if<
boost::is_convertible<OtherValue*,Value*>
, enabler
>::type = enabler()
)
: m_node(other.m_node) {}
Wrap UpНа этом завершается наш учебникiterator_facade, но прежде чем вы прекратите чтение, мы настоятельно рекомендуем вам взглянуть наiterator_adaptor. Есть еще один способ подойти к написанию этих итераторов, которые могут быть даже лучше. Статья Iterator Facade раздела может быть полезна для разработчиков на c++ и boost. :: Главная :: ::
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
©KANSoftWare (разработка программного обеспечения, создание программ, создание интерактивных сайтов), 2007 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||