C++ эффективная реализация классов-оболочек

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

Я заинтересован в выяснении того, что является самым умным способом размещения класса-обертки в качестве абстракции во всех этих библиотеках, чтобы стандартизировать эти операции в отношении остальной части моего кода. Мой нынешний подход основан на шаблоне Curiously Recurring Template Pattern и том факте, что C++11 gcc достаточно умен, чтобы встроить виртуальные функции в нужных условиях.

Это интерфейс оболочки, который будет доступен бизнес-логике:

template <class T>
class ITensor {

    virtual void initZeros(uint32_t dim1, uint32_t dim2) = 0;
    virtual void initOnes(uint32_t dim1, uint32_t dim2) = 0;
    virtual void initRand(uint32_t dim1, uint32_t dim2) = 0;

    virtual T mult(T& t) = 0;
    virtual T add(T& t) = 0;
};

А вот реализация этого интерфейса с использованием, например, Armadillo

template <typename precision> 
class Tensor : public ITensor<Tensor<precision> >
{
  public:

    Tensor(){}
    Tensor(arma::Mat<precision> mat) : M(mat) { }
    ~Tensor(){}

    inline void initOnes(uint32_t dim1, uint32_t dim2) override final
        {  M = arma::ones<arma::Mat<precision> >(dim1,dim2); }
    inline void initZeros(uint32_t dim1, uint32_t dim2) override final
        { M = arma::zeros<arma::Mat<precision> >(dim1,dim2);}
    inline void initRand(uint32_t dim1, uint32_t dim2) override final 
        { M = arma::randu<arma::Mat<precision> >(dim1,dim2);}

    inline Tensor<precision> mult(Tensor<precision>& t1) override final
    {
        Tensor<precision> t(M * t1.M);
        return t;
    }

    inline Tensor<precision> add(Tensor<precision>& t1) override final
    {
        Tensor<precision> t( M + t1.M);
        return t;
    }

    arma::Mat<precision> M;
};

Вопросы:

  1. Имеет ли смысл использовать CRTP и встраивание в этом сценарии?
  2. Можно ли это улучшить с точки зрения оптимизации производительности?

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

Вы заметите, что базовый класс называется "Tensor", а не чем-то более конкретным, например "ArmadilloTensor" (в конце концов, базовый класс реализует методы ITensor с использованием методов Armadillo). Я сохранил имя как есть, потому что согласно моему нынешнему замыслу, использование полиморфизма больше связано с ощущением формализма, чем с чем-либо еще. План состоит в том, чтобы код проекта был осведомлен о классе Tensor, который предлагает функции, указанные в ITensor. Для каждой новой библиотеки, которую я хочу сравнить, я бы просто написал новый класс "Tensor" в новом модуле компиляции, упаковал результаты компиляции в архив.a, а при выполнении теста сравнительного анализа связал код бизнес-логики с этим библиотека. Переключение между различными реализациями становится вопросом выбора реализации Tensor для связи. Для базового кода все равно, реализованы ли методы Tensor Armadillo или что-то еще. Преимущества: позволяет избежать наличия кода, который знает о каждой библиотеке (все они независимы), и не требуется никаких изменений времени компиляции в базовом коде для использования новой реализации. Итак, почему полиморфизм? Я просто хотел как-то формализовать функции, которые должны быть реализованы любой новой библиотекой, добавленной в тест. В действительности, базовый код будет работать с ITensors в параметрах функции, но затем потенциально static_cast их до Tensors в самих телах методов.

1 ответ

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

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

Но в вашем случае у вас нет разных реализаций одного интерфейса. Ваш ITensor Шаблон генерирует разные классы и каждую финальную реализацию вашего Tensor происходит из определенной базы.

Учтите, что ваш код пользователя выглядит примерно так:

template<typename T>
void useTensor(ITensor<T>& tensor);

и вы можете предоставить свой Tensor реализация. Это почти так же, как

template<typename T>
void useTensor(T& tensor);

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

Другие вопросы по тегам