Отправляет email-рассылки с помощью сервиса Sendsay

C++ для всех

  Все выпуски  

C++ для всех


Информационный Канал Subscribe.Ru

C++ для всех. Выпуск 8

Вложенные шаблонные классы. Наследование шаблонных классов. Итераторы

Здравствуйте, уважаемые подписчики

Сегодня мы поговорим о вложенных шаблонных классах и итераторах, и если тема итераторов вроде бы понятна, то может возникнуть вопрос, а чем отличаются вложенные шаблонные классы от обычных вложенных классов и зачем нужно было выделять это в отдельное понятие. Отвечу так: объявление и использование шаблонных вложенных классов ничем принципиально не отличается от обычных вложенных классов, но у многих начинающих программистов именно данная тема вызывает множество проблем при написании программ, поэтому мы и остановимся на ней более подробно.

Вложенные шаблонные классы

Итак, как выглядит объявление вложенного шаблонного класса:


 template<class ta>
 class A
 {
 public:
  template<class tai>
  class AI
  {
  public:
   AI(ta ta1,tai tai1):ta_(ta1),tai_(tai1){}

  private:
   ta ta_;
   tai tai_;
  };
 };

 int main()
 {
  A<int> a;
  A<int>::AI<int> ai(10,20);
  return 0;
 }

В функции main() показана форма вызова конструктора вложенного шаблонного класса. А вот так используют вложенные шаблонные классы внутри родительского класса:


 template<class ta>
 class A
 {
 public:
  template<class tai>
  class AI
  {
  public:
   void func(){}
  };
 public:
  void myfunc(){
   AI<double> a;
   a.func();
  }
 };

 А теперь попробуем скомпилировать следующий пример:

 template<class ta>
 class A
 {
 public:
  template<class tai>
  class AI
  {
  public:
   AI(const ta &v1,const tai &v2){}
  };
 public:
  void myfunc(){
   AI<double> a;
   a.func();
  }
 };

 int main()
 {
  A<int> a;
  //a.myfunc();
  return 0;
 }

Пример компилируется нормально, а теперь раскомментируем строку с функцией a.myfunc(), все - компилятор ругается. Данный случай - особенность шаблонов, компилятор не проверяет ни параметры ни вызываемые функции до тех пор, пока они не используются. У этого поведения две стороны - одна хорошая, другая не совсем хорошая. Хорошо то, что мы имеем возможность вызывать от передаваемого шаблона-параметра любые функции, которые как планируется у него есть, а недостаток (здесь скорее проявляется небрежность программиста) Вы видели на выше приведенном примере. Данные особенности поведения шаблонов позволяют писать такой код:


 template<class ta>
 class A
 {
 public:
  template<class tai>
  class AI
  {
  public:
   AI(const ta &v1,const tai &v2)
   {
    v1.showValue();
    v1.checkValue(10);
   }
  };
 public:
  void myfunc(){
   AI<double> a(100,20.3);
  }
 };

 int main()
 {
  A<int> a;
  //a.myfunc();
  return 0;
 }

В данном примере обратите внимание на конструктор вложенного шаблонного класса AI(const ta &v1,const tai &v2). В теле конструктора вызываются две функции showValue() и checkValue(10) от типа-шаблона, причем в данном случае компилятор не проверяет, есть ли у данного типа такие функции-члены. Раскомментировав в функции main() строку a.myfunc() мы получим ошибку компиляции. А теперь добавим кое-что к этому примеру:


 struct SBase{
  int val;
  
  SBase(int v=0):val(v){}
  
  void showValue()const{ printf("value: %d\n",val); }
  void checkValue(int)const{}  
 };

 template<class ta=SBase>
 class A
 {
 public:
  template<class tai>
  class AI
  {
  public:
   AI(const ta &v1,const tai &v2)
   {
    v1.showValue();
    v1.checkValue(10);
   }
  };
 public:
  void myfunc(){
   AI<double> a(100,20.3);
  }
 };

 int main()
 {
  A<SBase> a;
  a.myfunc();

  A<> a1;
  a1.myfunc();

  return 0;
 }

Все прекрасно работает. Обратите внимание на строку AI<double> a(100,20.3), здесь в качестве первого параметра передано число, т.е. предполагается, что шаблонный тип имеет конструктор с типом int - в нашем случае именно для этого определен конструктор структуры SBase - SBase(int v=0):val(v){}.

Теперь рассмотрим такой пример:

 
 template<class T>
 class A
 {
  template<class T1>
  class Iterator
  {
  private:
   void calc(){}
  };
 public:
  void func(){
   Iterator<int> it;
   it.calc();
  }
 };
 
 int main()
 {
  A<double> a;
  a.func();

  return 0;
 }

В этом примере была предпринята попытка обратится к private-функции вложенного класса - компилятор, естественно, нам этого не позволил. Чтобы исправить ситуацию нужно объявить родительский класс дружественным для вложенного. Делается это так:


 template<class T>
 class A
 {
  template<class T1>
  class Iterator
  {
   friend class A<T>;
  private:
   void calc(){}
  };
 public:
  void func(){
   Iterator<int> it;
   it.calc();
  }
 };
 
 int main()
 {
  A<double> a;
  a.func();

  return 0;
 }

Теперь все работает. Как варианты возможного использования friend-спецификатора используются такие объявления:


 template<class T>
 class A
 {
  friend class B<int>;
  template<class T1> friend class C;
  
  template<class T1> friend void func(int,T1);
 }; 

Наследование шаблонных классов

Механизм наследования шаблонных классов практически ничем не отличается от механизма наследования обычных классов, отличия только в необходимости объявлять шаблонные типы-параметры. Смотрим пример:

 
 template<class T>
 class A
 {
 public:
  A(const T &t_):t(t_){}
 private:
  T t;
 };

 template<class T1>
 class B : public A<T1>
 {
 public:
  B(const T1 &t_):A<T1>(t_){}
 };

 int main()
 {
  B<double> b(10.0);
  return 0;
 }

Наследование шаблонных классов ничем не отличается от наследования обычных классов, поэтому здесь я расскажу скорее о применении на практике полезных особенностей шаблонных классов.

Итак, проблема: имеем несколько сходных классов, необходимо перекрыть одну (или несколько) виртуальную функцию. Подразумевается, что данные базовые классы логически связаны, т.е. перекрываемая виртуальная функция выполняет одинаковое для всех классов действие, например возвращает координату объекта или еще что-либо. Можно решить двумя путями:

  1. От каждого класса произвести потомка с перекрытой соответствующей функцией. Способ абсолютно реальный и корректный, но мы условились, что содержимое перекрытой функции одинаковое, поэтому получим дублирование кода в нескольких классах. Причем чем больше функций нужно перекрыть, тем больше придутся дублировать одно и то же.
  2. Воспользуемся шаблонами. Смотрим пример:

 class A
 {
 public:
  virtual void print(){ printf("A::print()\n"); }
 };

 class B
 {
 public:
  virtual void print(){ printf("B::print()\n"); }
 };

 class C
 {
 public:
  virtual void print(){ printf("C::print()\n"); }
 };

 template<class T>
 class D : public T
 {
 public:
  virtual void print(){ printf("template D::print()\n"); }
 };

 int main()
 {
  D<A> a1;
  a1.print();
  
  D<B> b1;
  b1.print();
  
  D<C> c1;
  c1.print();

  return 0;
 }

На экране видим:


 template D::print()
 template D::print()
 template D::print()

На мой взгляд, данный способ решения проблемы наиболее привлекателен. Также шаблоны могут участвовать в иерархии и несколько по другому:


 class A
 {
 public:
  void calc(){}
 };
 
 template<class T>
 class B : public T
 {
 public:
  void print(){}
 };

 class C : public B<A>
 {
 public:
  void anyfunc(){}
 };
 
 int main()
 {
  C c;
  c.print();
  c.anyfunc();
  c.calc();

  return 0;
 }

В вышеприведенном примере мы дополнили базовые классы A и B функцией anyfunc(). Рекомендую внимательно присмотреться к наследованию с участием шаблонов, - в реальных примерах польза может быть довольна ощутимой. К этой же рекомендации добавлю еще одну, везде, где используется одинаковый интерфейс при работе с различными типами данных, старайтесь работу организовывать через шаблоны - это очень выразительный инструмент, возможности которого практически безграничны.

Пожалуй, на этом и закончим с шаблонами. Более внимательно мы их изучим в дальнейшем при рассмотрении более сложных практических задач.

Итераторы

Достаточно часто при написании программ используются различные коллекции однотипных элементов,- большинство STL классов представляют собой коллекции: std::vector, std::list ... Так вот, для доступа к элементам коллекции и были придуманы итераторы. По сути, итератор - это механизм доступа к определенному элементу коллекции. Бывают двух типов: активные и пассивные, также возможен вариант итератора с поведением обоих типов.

Активные итераторы - это итераторы, которые самостоятельно перемещаются к следующему элементу коллекции. Как правило определяются внутри класса. Активные итераторы просто обязаны знать о механизмах коллекции, т.к. управление итератором происходит через его собственные функции:


 template<class T>
 class List
 {
 public:
  class Iterator
  {
  public:
   bool operator!=(const Iterator &s)const{ return true; }
   Iterator& operator++(){ /*...*/ return *this; }
   Iterator& operator--(){ /*...*/ return *this; }
  };
 public:
  Iterator begin()const{ return Iterator; }
  Iterator end()const{ return Iterator; }
 };
 
 int main()
 {
  List<int> list;
  ...
  for(List<int>::Iterator it=list.begin();it!=list.end();++it){
  }
 
  return 0;
 }

В отличие от активных, пассивные итераторы ничего не знают о внутренних механизмах коллекции. Все операции по перемещению итератора в коллекции реализовывает сама коллекция,- для этого используются функции коллекции с итератором в качестве параметра:


 class List
 {
 public:
  class Iterator
  {
  public:
   bool operator!=(const Iterator &s)const{ return true; }
  };
 public:
  Iterator begin()const{ return Iterator; }
  Iterator end()const{ return Iterator; }
  void next(Iterator&)const{}
  void prev(Iterator&)const{}
 };
 
 int main()
 {
  List<int> list;
  ...
  for(List<int>::Iterator it=list.begin();it!=list.end();list.next(it)){
  }
  
  return 0;
 }

Также итераторы могут быть определены как вложенные классы (вышеприведенные примеры) и как независимые классы:

 
 template<class T>
 class ListIterator
 {
 public:
  bool operator!=(const ListIterator<T> &s)const{ return true; }
 };

 template<class T>
 class List
 {
 public:
  ListIterator<T> begin()const{ return ListIterator<T>(); }
  ListIterator<T> end()const{ return ListIterator<T>(); }
  void next(ListIterator<T>&)const{}
  void prev(ListIterator<T>&)const{}
 };

 int main()
 {
  List<int> list;
  ...
  for(ListIterator<int> it=list.begin();it!=list.end();list.next(it)){
  }
  
  return 0;
 }

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

В заключение об итераторах добавлю, что практически для всех активных итераторов должны быть реализованы такие функции (T - это тип-параметр шаблона):


 Iterator& operator++();
 const Iterator operator++(int);
 Iterator& operator--();
 const Iterator operator--(int);

 T* operator->()const;
 T* operator*()const;

Теперь вкратце рассмотрим класс списка указателей, который был реализован в прошлом выпуске (реализацию для упрощения я убрал - остались одни объявления):


 template<class type>
 class TPtrList
 {
 protected:
  struct SNode{
   type *data;
   SNode *next;
  };

 public:
  class Iterator{
   friend class TPtrList;
  public:
   Iterator();
   Iterator(const Iterator &s);
   bool isValid()const;

   Iterator& operator++();
   const Iterator operator++(int);

   type* operator->()const;
   type* operator*()const;

  protected:
   Iterator(SNode *n);

  private:
   SNode *curr;
  };

  TPtrList(bool autodelete=false);
  TPtrList(const TPtrList<type> &s);
  virtual ~TPtrList();

  bool autoDelete()const;
  void setAutoDelete(bool ad);

  int count()const;
  bool isEmpty()const;

  void clear();
  bool removeCurrent();

  void append(type *d);
  bool remove(type *d);
  type* find(type *d)const;

  type* first();
  type* last();
  type* next();

  Iterator begin()const;
  Iterator end()const;

  const TPtrList& operator=(const TPtrList &s);

 private:
  SNode *ifirst,*ilast;
  SNode *icurr;
  int count_i;
  bool is_autodelete;
 };

Итак, объявлен шаблонный класс TPtrList с вложенным классом-итератором TPtrList::Iterator для перемещения по списку указателей, объявлена защищенная структура SNode для организации хранения данных. Структура SNode объявлена защищенной, т.к. нам нужно скрыть внутреннюю организацию нашего класса - все, что нужно знать конечному пользователю вернет итератор.

Данный класс обладает такой функциональностью: добавление-удаление указателя на объект, поиск указателя, обход вперед списка через итератор, удаление при необходимости объекта, указатель на который хранится в списке (если autoDelete() установлено в true). Чего здесь нет: здесь нет поиска по индексу (через оператор [] или функцию at()), здесь нет обхода списка элементов назад, остальное можете придумать сами...

В-общем Вы поняли - это домашнее задание, - жду решений.


На сегодня все. Пожелания, предложения, вопросы прошу на iqsoft@cg.ukrtel.net

В следующем выпуске мы поговорим о приведении типов.


(с) Юрий Гордиенко



http://subscribe.ru/
E-mail: ask@subscribe.ru
Отписаться
Убрать рекламу

В избранное