C — как структурировать композиционную иерархию типов для масштабируемости?

#c #templates #typedef #composition #template-templates

#c #шаблоны #typedef #композиция #шаблон-шаблоны

Вопрос:

У меня есть три класса, которые можно комбинировать статически. Класс самого низкого уровня A и является шаблонным классом с одним параметром. Это может быть, скажем, int . Тогда у меня есть класс более высокого уровня, B , который зависит от типа T и A . Наконец, у меня есть класс более высокого уровня C , который зависит от T , A и B .

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

 // template-template style:
template <typename T>
struct A {
  T at_;
};

template <typename T,
          template <typename> class A>
struct B {
  A<T> ba_;
  T bt_;
};

template <typename T,
          template <typename> class A,
          template <typename, template <typename> class> class B>
struct C {
  B<T, A> cb_;
  A<T> ca_;
  T ct_;
};


// public-scoped typedef style:
template <typename T>
struct A2 {
  typedef T TType;
  T at_;
};

template <class A>
struct B2 {
  typedef A AType;
  AType ba_;
  typename A::TType bt_;
};

template <class B>
struct C2 {
  typedef B BType;
  B cb_;
  typename B::AType ca_;
  typename B::AType::TType ct_;
};

// ...
// client code:
A<int> MyA;
B<int, A> MyB;
C<int, A, B> MyC;

A2<int> myA2;
B2<A2<int> > myB2;
C2<B2<A2<int> > > myC2;
 

Теперь рассмотрим новый класс D, который расширяет шаблон — при первом подходе объявление шаблона становится почти полностью неуправляемым:

 // horrendous to define:
template <typename T,
          template <typename> class A,
          template <typename,
                    template <typename> class> class B,
          template <typename,
                    template <typename> class,
                    template <typename, template <typename> class> class> class C>
struct D {
  C<T, A, B> dc_;
  B<T, A> db_;
  A<T> da_;
  T dt_;
};

// but nice to instantiate:
D<int, A, B, C> MyD;
 

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

Альтернативно:

 template <class C>
struct D2 {
  typedef C CType;  // <-- not required right now, but better put it in just-in-case...
  C dc_;
  typename C::BType db_;
  typename C::BType::AType da_;
  typename C::BType::AType::TType ct_;
};
 

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

Мои вопросы — какие еще плюсы / минусы каждого подхода я пропустил, и какой подход, вероятно, будет работать лучше всего при создании растущей библиотеки классов, которые, как правило, объединяются статически, как это? Является ли один стиль более распространенным, чем другой? Есть ли альтернативы, которые здесь не рассматриваются?

Примечание о практических приложениях — у меня уже есть реальное применение этого, которое включает в себя специальные типы числовых значений, которые ведут себя определенным образом. Эти значения заключены в классы более высокого уровня, каждый из которых предоставляет дополнительный уровень функциональности — по одному на уровень. Например, один уровень обеспечивает механизм обратного вызова, другой обеспечивает кэширование, другой обеспечивает проверку значения, другой предоставляет функцию преобразования. Более высоким уровням может потребоваться знать больше, чем непосредственно нижестоящему слою, следовательно, требуется знание типов, используемых для создания нижестоящих типов. На каждом уровне существует несколько разных реализаций, отсюда и необходимость указывать так много типов. Различные разделы этой иерархии могут использоваться в другом коде, и, в свою очередь, этот код также может оказаться частью иерархии. Это довольно классическая «композиционная» модель, которая наращивает функциональность путем компоновки классов более низкого уровня, но создается статически.

Комментарии:

1. Как насчет использования неопределенной переменной базы, скажем template <typename... Ts> class Base; , и специализации тех, которые вас интересуют? Например, у вас будет специализация template <typename T> class Base<T, A<T>, B<T,A<T>>{//Appropriate typedefs};

2. @Pradhan интересная идея — я задавался вопросом, могут ли помочь переменные шаблоны, но не был уверен, как они будут использоваться. Не могли бы вы предоставить более полный пример в качестве ответа, пожалуйста?