Эмуляция Динамической Отправки в C++ на основе Шаблонных Параметров

Это в большой степени упрощено ради вопроса. Скажите, что у меня есть иерархия:

struct Base {
    virtual int precision() const = 0;
};

template<int Precision>
struct Derived : public Base {

    typedef Traits<Precision>::Type Type;

    Derived(Type data) : value(data) {}
    virtual int precision() const { return Precision; }

    Type value;

};

Я хочу нешаблонную функцию с подписью:

Base* function(const Base& a, const Base& b);

Где определенный тип результата функции является тем же типом как какой бы ни из a и b имеет большее Precision; что-то как следующий псевдокод:

Base* function(const Base& a, const Base& b) {

    if (a.precision() > b.precision())

        return new A( ((A&)a).value + A(b.value).value );

    else if (a.precision() < b.precision())

        return new B( B(((A&)a).value).value + ((B&)b).value );

    else

        return new A( ((A&)a).value + ((A&)b).value );

}

Где A и B определенные типы a и b, соответственно. Я хочу function работать независимо от сколько инстанцирований Derived существуют. Я хотел бы избежать крупной таблицы typeid() сравнения, хотя RTTI прекрасен в ответах. Какие-либо идеи?

5
задан Jon Purdy 12 March 2010 в 22:22
поделиться

3 ответа

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

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

Derived<Precision> *operation(Base& arg2) {
  Derived<Precision> *ptr = new Derived<Precision>;
  // ...
  return ptr;
}

Затем вы можете определить функцию таким образом и выполнять диспетчеризацию косвенно:

Base* function(const Base& a, const Base& b) {
  if (a.precision() > b.precision())
    return a.operation(b);
  else 
    return b.operation(a);
}

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

Кроме того, здесь не учитывается тот факт, что вам нужен способ для a.operation получить подходящую форму b.value без знания производного типа b .Вам придется решить эту проблему самостоятельно - обратите внимание, что (по той же логике, что и раньше) невозможно решить эту проблему с помощью шаблона типа b , потому что вы отправляете во время выполнения. Решение зависит от того, какие именно типы у вас есть, и есть ли способ для типа более высокой точности извлекать значение из объекта Производный с равной или меньшей точностью, не зная точного типа. этого объекта. Это может быть невозможно, и в этом случае у вас есть длинный список совпадений по идентификаторам типов.

Однако это необязательно делать в операторе switch. Вы можете дать каждому производному типу набор функций-членов для преобразования его в функцию большей точности. Например:

template<int i>
upCast<Derived<i> >() {
  return /* upcasted value of this */
}

Тогда ваша функция-член operator может работать с b.upcast , и ей не нужно будет явно выполнять приведение для получения значения. того типа, который ему нужен. Возможно, вам придется явно создать экземпляры некоторых из этих функций, чтобы заставить их скомпилировать; Я недостаточно поработал с RTTI, чтобы сказать наверняка.

По сути, проблема в том, что если у вас есть N возможных уточнений, у вас есть N N возможных комбинаций, и каждая из них фактически должна иметь отдельно скомпилированный код. Если вы не можете использовать шаблоны в своем определении функции , тогда вы должны иметь скомпилированные версии всех N N этих возможностей, и каким-то образом вы должны указать компилятору сгенерировать их все, и каким-то образом вам нужно выбрать правильный вариант для отправки во время выполнения.Уловка с использованием функции-члена устраняет один из этих факторов N, но другой остается, и нет способа сделать его полностью универсальным.

3
ответ дан 14 December 2019 в 04:35
поделиться

То, о чем вы просите, называется множественная диспетчеризация, она же мультиметоды. Это не является особенностью языка C++.

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

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

Если предположить, что существует n производных классов, то будет один метод для разрешения первого параметра, n для разрешения второго, n*n для разрешения третьего и так далее - в худшем случае. Это довольно много ручной работы, и использование условных блоков на основе typeid может быть проще для начальной разработки, но для обслуживания надежнее использовать redispatch.

class Base;
class Derived1;
class Derived2;

class Base
{
  public:
    virtual void Handle (Base* p2);

    virtual void Handle (Derived1* p1);
    virtual void Handle (Derived2* p1);
};

class Derived1 : public Base
{
  public:
    void Handle (Base* p2);

    void Handle (Derived1* p1);
    void Handle (Derived2* p1);
};

void Derived1::Handle (Base* p2)
{
  p2->Handle (this);
}

void Derived1::Handle (Derived1* p1)
{
  //  p1 is Derived1*, this (p2) is Derived1*
}

void Derived1::Handle (Derived2* p1)
{
  //  p1 is Derived2*, this (p2) is Derived1*
}

//  etc

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

Шаблон проектирования посетитель тесно связан (по сути, реализован с использованием) redispatch IIRC.

Другой подход - использовать язык, предназначенный для решения этой проблемы, и есть несколько вариантов, которые хорошо работают с C++. Один из них - использовать treecc - специфический для данной области язык для работы с узлами AST и множественными операциями диспетчеризации, который, подобно lex и yacc, генерирует "исходный код" на выходе.

Все, что он делает для обработки решений о диспетчеризации, это генерирует операторы switch на основе идентификатора AST-узла - который может быть и динамически типизированным идентификатором класса значений, IYSWIM. Однако это операторы переключения, которые вам не нужно писать или поддерживать, что является ключевым отличием. Самая большая проблема, с которой я столкнулся, заключается в том, что AST-узлы имеют подстроенную обработку деструкторов, что означает, что деструкторы для данных-членов не вызываются, если вы не приложите особых усилий - т.е. это лучше всего работает с POD-типами для полей.

Другой вариант - использовать препроцессор языка, который поддерживает мультиметоды. Таких препроцессоров было несколько, отчасти потому, что у Струструпа в свое время были довольно хорошо проработанные идеи по поддержке мультиметодов. CMM - один из них. Doublecpp - еще одна. Еще одним является Frost Project. Я считаю, что CMM ближе всего к тому, что описал Строуструп, но я не проверял.

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

Как это часто бывает, не существует единственно верного способа сделать это, по крайней мере, в C++.

4
ответ дан 14 December 2019 в 04:35
поделиться

Во-первых, вы хотите сделать ваш член precision значением static const int, а не функцией, чтобы вы могли работать с ним во время компиляции. В Derived это будет:

static const int precision = Precision;

Затем вам понадобятся вспомогательные структуры для определения наиболее точного класса Base/Derived. Во-первых, нужна общая структура-помощник для выбора одного из двух типов в зависимости от булевой:

template<typename T1, typename T2, bool use_first>
struct pickType {
  typedef T2 type;
};

template<typename T1, typename T2>
struct pickType<T1, T2, true> {
  typedef T1 type;
};

Тогда, pickType::type разрешится в T1, если use_first будет true, а иначе в T2. Итак, мы используем это для выбора наиболее точного типа:

template<typename T1, typename T2>
struct mostPrecise{
  typedef pickType<T1, T2, (T1::precision > T2::precision)>::type type;
};

Теперь, mostPrecise::type даст вам любой из двух типов с большей точностью. Таким образом, вы можете определить свою функцию так:

template<typename T1, typename T2>
mostPrecise<T1, T2>::type function(const T1& a, const T2& b) {
  // ...
}

И вот она у вас есть.

1
ответ дан 14 December 2019 в 04:35
поделиться
Другие вопросы по тегам:

Похожие вопросы: