Как определить абстрактные базовые классы для поддержки произвольных (но определенных во время компиляции) функций? - PullRequest
4 голосов
/ 12 ноября 2009

У меня есть следующая проблема дизайна C ++, и я был бы очень признателен за любое предложение / решение. Обратите внимание, что мой опыт работы не в области компьютерных наук, поэтому я могу упустить какое-то очевидное решение.

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

Example1:

class B
{
public:
    virtual double f( double x ) = 0;
};

class D1 : public B
{
public:
    double f( double x ) const 
    {
    return 0.0;
    }
};    

class D2 : public B
{    
public:
    double f( double x ) const 
    {
    return 1.0;
    }
};

Таким образом, я могу красиво отделить интерфейс от реализации. Этот подход также довольно быстр (и то, над чем я работаю, это числовая библиотека, это важно: P).

Теперь проблема, с которой я сталкиваюсь, заключается в следующем.

У меня есть набор «функциональных возможностей», которые можно суммировать с помощью функций (определенных ниже) f (), g () и h () Обратите внимание, что все эти функции обычно отличаются аргументами и типами возвращаемых данных.

Предположим, у меня есть некоторый код, который ожидает указатель на объект, который реализует функции f () и g (). Я хотел бы иметь возможность передавать что-то, что имеет «более или равные» функциональные возможности, например что-то, что поддерживает f (), g () и h ().

Чтобы лучше объяснить себя, приведу код. Обратите внимание, что вместо множественного наследования я мог бы использовать подход «вложенного наследования», как в boost :: operator. Дело в том, что у меня никогда не будет случая, когда f () будет таким же, как g (). Все функции разные. Проблема в том, что для этой работы мне нужно использовать reinterpret_cast, как в примере ниже (так что это не совсем решение):

Пример2:

class F {
public:
    virtual double f( double x ) = 0;
};

class G {
public:
    virtual double g( double x ) = 0;
};

class H {
public:
    virtual double h( double x ) = 0;
};

class N {};

template<class T1, class T2=N, class T3=N>
class Feature : public T1 , public T2 , public T3
{
};

template<class T1, class T2>
class Feature<T1,T2,N> : public T1, public T2
{
};

template<class T1>
class Feature<T1,N,N> : public T1
{
};

//Supp for Supports/Implements
class SuppFandG : public Feature<F,G>
{
public:
    double f( double x ) { return 0.0; }
    double g( double x ) { return 1.0; } 
};

class SuppFandH : public Feature<F,H>
{
public:
    double f( double x ) { return 0.0; }
    double h( double x ) { return 1.0; } 
};

class SuppFandGandH : public Feature<F,G,H>
{
public:
    double f( double x ) { return 0.0; }
    double g( double x ) { return 1.0; }
    double h( double x ) { return 2.0; }
};

int main()
{
    Feature<F,G>* featureFandGPtr;
    Feature<F,H>* featureFandHPtr;
    Feature<H,F>* featureHandFPtr;
    Feature<F,G,H>* featureFandGandHPtr;

    SuppFandGandH suppFandGandH;
    featureFandGandHPtr = &suppFandGandH;

    //featureFandGPtr = featureFandGandHPtr; //Illegal. static_cast illegal too.
    //the reason to do this is that I would like to pass a pointer to an object 
    //of type Feature<F,G,H> to a function (or constructor) that expects a pointer to Feature<F,G> 
    featureFandGPtr = reinterpret_cast< Feature<F,G>* >( featureFandGandHPtr );
    featureFandHPtr = reinterpret_cast< Feature<F,H>* >( featureFandGandHPtr );
    featureHandFPtr = reinterpret_cast< Feature<H,F>* >( featureFandGandHPtr );

    featureFandGPtr->f( 1.0 );
    featureFandGandHPtr->h( 1.0 );
}

Или я могу попытаться построить иерархию наследования, но изменив определение Feature, но в следующем примере происходит сбой профессионального компилятора Visual Studio 2008, поэтому я не могу его протестировать.

Пример 3:

//This will not work, Visual studio 2008 professional crash.
template<class T1, class T2=N, class T3=N>
class Feature : public Feature<T1,T2> , public Feature<T1,T3> , public Feature<T2,T3>
{
};

template<class T1, class T2>
class Feature<T1,T2,N> : public Feature<T1>, public Feature<T2>
{
};

template<class T1>
class Feature<T1,N,N> : public T1
{
};

При таком подходе у меня все еще есть проблемы 1) Функция логически эквивалентна (для чего я хочу достичь) функции, но их типы разные. Однако это может быть решено с помощью какого-то необычного метапрограммирования с использованием библиотеки повышения MPL (всегда «сортируйте» типы), поэтому для простоты предположим, что это не проблема.

2) Проблема нескольких баз, и я хочу избежать виртуального наследования через виртуальные базы (снижение производительности). Это, вероятно, решаемо с помощью директив внутри специализаций Feature.

Тем не менее, я не уверен на 100%, что смогу сделать эту работу, и она не будет хорошо масштабироваться для большого количества функций. Фактически количество элементов, составляющих иерархию, определяется биномиальным коэффициентом, почти факториальным: F -> F (1) F, G -> FG, F, G (3)
F, G, H -> FGH, FG, GH, FH, F, G, H (7)

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

1) Код должен иметь производительность во время выполнения, эквивалентную примеру 1.

2) Я хочу иметь возможность легко указывать некоторый набор функций и иметь возможность «передавать» любые указатели на объекты, которые имеют эту (и обычно дополнительную) функциональность.

3) Я хочу, чтобы код, зависящий от функций f () и g (), не требовал перекомпиляции всякий раз, когда я рассматриваю новую функцию h () где-то еще.

4) Я не хочу шаблонировать все, что хочет использовать такие функции (почти весь код). Должно быть какое-то «разделение», см. Пункт 3.

Глядя в (числовые) библиотеки, я обычно нашел два подхода:

1) Определите огромный абстрактный базовый класс B, который имеет f (), g (), h (), ...... Проблемы: всякий раз, когда я хочу добавить новую функцию z (), необходимо изменить B, все необходимо перекомпилировать (даже если этот код вообще не заботится о z ()), все существующие реализации D1, D2 , ... of B необходимо изменить (обычно, если они выбрасывают исключение для z (), для новой реализации, которая поддерживает z ()).Решение постепенного увеличения B, когда мне нужно добавить функции, не подходит для рассматриваемой проблемы, так как функции f () и g () действительно «так же важны», как h () и i (), и "более простой", чем другие.

2) Разделите все функции и используйте один указатель для каждой функции. Тем не менее, это является обременительным для пользователя (в большинстве ситуаций необходимо переносить 4 или более указателей), и для рассматриваемой проблемы этот подход не является оптимальным (здесь на самом деле это 1 объект, который может или не может что-то делать, фактически вызов функции f () изменит результат, полученный с помощью функции g () и наоборот).

Заранее благодарю за помощь.

КрАО.

Ответы [ 4 ]

1 голос
/ 13 ноября 2009

Теперь это интересная проблема!

Очевидное (и простое) решение - сойти с ума по шаблону. Если бы каждой функции требовался только интерфейс (а не определенный тип), это помогло бы решить большинство ваших проблем. Но, конечно, это имеет свое собственное наказание с точки зрения зависимости, и вы отвергаете это решение в пункте 4).

Динамическое литье?

struct Feature { virtual ~Feature() {} };

class F: public Feature {};
class G: public Feature {};

Теперь класс, который реализует F и G, объявлен так

class Impl: public F, public G {};

И метод, требующий F и G, как это

void method(Feature const& i)
{
  F const& myF = dynamic_cast<F const&>(i);
  G const& myG = dynamic_cast<G const&>(i);

  myF.f(2.0);
  myG.g(2.0);
}

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

Пересылка шаблонов

Это, однако, влечет за собой другое решение:

namespace detail
{
  void methodImpl(F const& f, G const& g);
}

template <class T>
void method(T const& t)
{
  detail::methodImpl(t,t);
}

Это объединяет подход BostonLogan с лучшим интерфейсом. Ни один пользовательский код никогда не должен упоминать пространство имен detail (которое легко проверить), и если это так, вам гарантировано, что никто не использует два разных объекта для вызова methodImpl.

Это, кажется, покрывает большинство ваших потребностей:

  1. Производительность во время выполнения эквивалентна вашему примеру
  2. T может наследоваться от H или Z, вам все равно, если он наследует от F и G, код будет компилироваться
  3. Добавление другой функции ничего не меняет
  4. ... ну, вы должны шаблонизировать интерфейс (method), но это пересылка с одной строкой к не шаблонному методу, который выполняет тяжелую работу.

Единственное, что меня раздражает, это то, что methodImpl фактически имеет 2 ссылки на один и тот же объект, что может повлечь за собой проблемы в будущем.

Избавляемся от нескольких ссылок

Это будет непросто, но мы собираемся обернуть этот объект и делегировать работу.

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

Для этого нам понадобятся еще 2 вещи:

  • Каждый Feature должен объявить экспедитора
  • Нам нужна система агрегирования экспедиторов

Давайте рассмотрим первый пункт:

class F
{
public:
  void f(double d);
  void f2(double d, double e) const;
};

class FForwarder
{
public:
  FForwarder(F& f) : m_object(f) {}

  void f(double d) { m_object.f(d); }
  void f2(double d, double e) const { m_object.f(d,e); }

private:
  F& m_object;
};

Достаточно просто, но громоздко.

Давайте рассмотрим агрегацию:

struct nil {};

template <class Head, class Tail>
struct Aggregator: Head, Aggregator<Tail::head, Tail::tail>
{
  typedef Head head;
  typedef Tail tail;

  template <class T>
  Aggregator(T& t) : Head(t), Aggregator<Tail::head, Tail::tail>(t) {}
};

template <class Head>
struct Aggregator<Head,nil> : Head
{
  typedef Head head;
  typedef nil tail;

  template <class T>
  Aggregator(T& t) : Head(t) {}
};

template <>
struct Aggregator<nil,nil>
{
  typedef nil head;
  typedef nil tail;

  template <class T>
  Aggregator(T&) {}
};

А теперь перейдем к использованию.

Либо, обременяя абонента бременем:

int method(Aggregator<FForwarder, Aggregator<GForwarder, HForwarder> >& fgh);

Или написав шаблон перенаправителя:

namespace detail
{
  typedef Aggregator<FForwarder, Aggregator<GForwarder, HForwarder> > methodImplArg;
  int methodImpl(methodImplArg& arg);
}

template <class T>
int method(T& t)
{
  detail::methodImplArg arg = detail::methodImplArg(t);
    //named temporary because it is passed by reference to non-const

  return detail::methodImpl(arg);
    // forward the result as well
};

Это решает проблему того, что methodImpl передается 2 аргумента довольно аккуратно, хотя это требует дополнительной работы ... Я предполагаю, что должен быть более простой способ, но пока не могу его точно определить.

0 голосов
/ 16 ноября 2009

Я решил эту проблему с помощью следующего кода. Я сделал не чистые абстрактные классы F, G, H, N просто для упрощения описания, но это можно легко изменить (используйте F в качестве тега, Feature для интерфейса).

Особенности:

class F {
public:
 virtual double f( double x )
 {
  return 10.0;
 };

 virtual double sample( const F& t , double x )
 {
  return f( x );
 };
};

class G {
public:
 virtual double g( double x )
 {
  return 10.0;
 };

 virtual double sample( const G& t , double x )
 {
  return g( x );
 };

};

class H {
public:
 virtual double h( double x )
 {
  return 10.0;
 };

 virtual double sample( const H& t , double x )
 {
  return h( x );
 };
};

class N {};

Я использую класс boost :: fusion map, но с некоторыми усилиями это можно переписать, чтобы избежать этого.

Класс Impl позволяет определять функции, упрощающие пользовательский интерфейс. Это необходимо, потому что я использую композицию / агрегацию вместо наследования, и поэтому класс, который реализует требуемую функцию, должен быть «выбран вручную».

template<class Derived, class T>
class Impl
{
 template<class S1, class S2>
 double sample( const S2& x )
 {
  return boost::fusion::at_key<S1>( static_cast<Derived*>(this)->container )->sample( S1() , x );
 }
};

template<class Derived>
class Impl<Derived,F>
{
public:

 double f( double x )
 {
  return boost::fusion::at_key<F>( static_cast<Derived*>( this )->container )->f( x );
 }
};

Это ключевой класс, который «агрегирует» функции T1, T2, T3. Ниже того же класса для функций T1, T2. Класс для T1 только оставлен как пользовательское упражнение:)

template<class T1, class T2=N, class T3=N>
class Rvg : public Impl<Rvg<T1,T2,T3>,T1>, public Impl<Rvg<T1,T2,T3>,T2>, public Impl<Rvg<T1,T2,T3>,T3>
{
public:

 typedef typename boost::fusion::map<
  boost::fusion::pair<T1,T1*>,
  boost::fusion::pair<T2,T2*>,
  boost::fusion::pair<T3,T3*> > container_type;

 container_type container;

public:

 Rvg()
 {}

 template<class S1>
 Rvg( S1* s1Ptr )
 :
 container( static_cast<T1*>( s1Ptr ), static_cast<T2*>( s1Ptr ), static_cast<T3*>( s1Ptr ) )
 {}

 template<class S1, class S2, class S3>
 Rvg<T1,T2,T3>& operator = ( const Rvg<S1,S2,S3>& rhs )
 {
  container = rhs.container;
  return *this;
 }

 template<class S>
 S& operator() ( S& s )
 {
  return *boost::fusion::at_key<S>( container );
 }

};

template<class T1, class T2>
class Rvg<T1,T2,N> : public Impl<Rvg<T1,T2,N>,T1>, public Impl<Rvg<T1,T2,N>,T2>
{
public:

 typedef typename boost::fusion::map<
  boost::fusion::pair<T1,T1*>,
  boost::fusion::pair<T2,T2*> > container_type;

 container_type container;

public:

 Rvg()
 {}

 template<class S1>
 Rvg( S1* s1Ptr )
 :
 container( static_cast<T1*>( s1Ptr ), static_cast<T2*>( s1Ptr ) )
 {}

 template<class S1, class S2, class S3>
 Rvg& operator = ( const Rvg<S1,S2,S3>& rhs )
 {
  container = rhs.container;
  return *this;
 }

 template<class S>
 S& operator() ( S& s )
 {
  return *boost::fusion::at_key<S>( container );
 }

};

//Rvg<T1,N,N> defined according to the same pattern.

Примеры классов, поддерживающих функции F, G, H.

class SuppFandGandH : public F , public G , public H
{
public:
 double f( double x ) { return 14.0; }
 double g( double x ) { return 15.0; }
 double h( double x ) { return 16.0; }

 double sample( const F& f , double x ) { return 17.0; }
 double sample( const G& g , double x ) { return 18.0; }
 double sample( const H& h , double x ) { return 19.0; }
};

Пример использования пользователем:

int main()
{
 SuppFandGandH suppFandGandH;

 Rvg<F,G,H> myRvgFGH( &suppFandGandH ); //Automatic construction

 myRvgFGH( F() ).f( 0.0 ); //Base calling
 myRvgFGH.sample<F>( 0.0 ); //Fwded calling
 myRvgFGH.f( 0.0 ); //Specific fwded calling

 myRvgFGH.sample<F>( 0.0 );
 myRvgFGH.sample<G>( 0.0 );
 myRvgFGH.sample<H>( 0.0 );

 Rvg<F,G> myRvgFG;
 myRvgFG = myRvgFGH; //Less from More

 Rvg<F,G> myRvgFG2( &suppFandGandH ); //Again automatic construction
 Rvg<F,H> myRvgFH( &suppFandGandH ); //The same
}

Это должно удовлетворять всем четырем требованиям.

0 голосов
/ 12 ноября 2009

В данном конкретном случае, на мой взгляд, краткий кодекс приносит больше вреда, чем пользы Почему бы не передать два (или три) указателя интерфейса в конструктор или функцию?

class F
{ 
public:
   virtual double f(double x) =0;
};

class G
{
public:
    virtual double g(double x) = 0;
};

class H
{
public:
    virtual double h(double x) = 0;
};

class FG : public F, public G
{
public:
    double f(double x)  {return 1.;}
    double g(double x)  {return 2.;}
};

class FGH : public F, public G, public H
{
public:
    double f(double x)  {return 1.;}
    double g(double x)  {return 2.;}
    double h(double x)  {return 3.;}
};

void Foo(F* pF, G* pG)
{
    pF->f(5.);
    pG->g(10.);
}

void Foo2(F* pF, G* pG, H* pH)
{
    pF->f(5.);
    pG->g(10.);
    pH->h(20.);
}

int main()
{
    FGH fgh;
    Foo(&fgh, &fgh);
    Foo2(&fgh, &fgh, &fgh);
}
0 голосов
/ 12 ноября 2009

Я бы пошел с написанным от руки подходом к вашему Примеру 3. Когда вы просматриваете свой код, вы обнаружите, что вы не используете все возможные комбинации функций; вместо этого вы используете небольшое подмножество. Вместо того, чтобы шаблоны генерировали все возможные комбинации, просто напишите те, которые вам нужны. Скажем, у вас есть F, G и H, и вы используете FGH, FG, GH и F, G, H по отдельности.

template <typename T1, typename T2 = N, typename T3 = N> struct Feature; 
template <typename T1> struct Feature<T1,N,N> : T1 { };
template <> struct Feature<F,G,N> : Feature<F>, Feature<G> { };
template <> struct Feature<G,H,N> : Feature<G>, Feature<H> { };
template <> struct Feature<F,G,H> : Feature<F,G>, Feature<G,H> { };

Если вы пропустите нужную комбинацию, компилятор скажет вам.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...