Канонический способ реализации шаблонных виртуальных функций - PullRequest
0 голосов
/ 11 февраля 2020

В моем приложении мне нужно абстрагировать шаблонный алгоритм, чтобы его можно было выбирать во время выполнения. Я вроде хочу «шаблонные виртуальные функции» (где и ifce, и impl создаются во время компиляции со списком разрешенных типов), но я понимаю, что не могу шаблонировать виртуальные функции в C ++. У меня было несколько идей на этом пути, и это лучшее, что я придумал до сих пор - но я действительно не доволен тем, насколько это сложно. Я пропускаю что-то более очевидное? Мне нужно, чтобы решение было быстрым, а также производным от общего базового класса.

#include <memory>
#include <iostream>

enum class calc_enum { calc_1, calc_2, calc_3 };

// implementation class
template<typename F, typename T, typename... U> struct algo_impl;

// base class
struct algo_base {};

// interface class
template<typename... U> struct algo_ifce;

template<>
struct algo_ifce<> : algo_base
{
    template<typename F, typename T>
    algo_ifce(std::shared_ptr<algo_impl<F, T>>&& ptr) 
        : ptr_{ std::move(ptr) }
    {}

    template<typename V>
    V calc_ifce(V val) {
        throw std::runtime_error("nope");
    }

    std::shared_ptr<void> ptr_;
};

template<typename U, typename... REST>
struct algo_ifce<U, REST...> : algo_ifce<REST...>
{
    using fn_type = U(*)(void*, U);

    template<typename F, typename T>
    algo_ifce(std::shared_ptr<algo_impl<F, T, U, REST...>>&& ptr_)
        : algo_ifce<REST...>(std::shared_ptr<algo_impl<F, T, REST...>>(std::move(ptr_))), fn_(algo_impl<F, T, U, REST...>::calc_static)
    {}

    template<typename V>
    V calc_ifce(V val) {
        if constexpr (std::is_same_v<U, V>) return calc_dispatch(val);
        else return algo_ifce<REST...>::template calc_ifce<V>(val);
    }

    U calc_dispatch(U val) { 
        return fn_(algo_ifce<REST...>::ptr_.get(), val);
    }

    fn_type fn_;
};

template<typename F, typename T>
struct algo_impl<F, T>
{
    algo_impl(T data) 
        : data_{ data }
    {}

    T data_;
};

template<typename F, typename T, typename U, typename... REST>
struct algo_impl<F, T, U, REST...> : algo_impl<F, T, REST...>
{
    algo_impl(T data) 
        : algo_impl<F, T, REST...>{ data }
    {}

    static U calc_static(void* ptr, U val) {
        return static_cast<algo_impl*>(ptr)->calc_algo(val); 
    }

    U calc_algo(U val) {
        return F::template calc<U>(val, algo_impl<F, T, REST...>::data_);
    }
};

// algo types
template<typename T>
struct calc_impl_1
{
    template<typename U> static U calc(U val, T data) {
        std::cout << "algo 1, T:" << typeid(T).name() << ", U:" << typeid(U).name() << std::endl;
        return static_cast<U>(data) + val;
    }
};

template<typename T>
struct calc_impl_2
{
    template<typename U> static U calc(U val, T data) {
        std::cout << "algo 2, T:" << typeid(T).name() << ", U:" << typeid(U).name() << std::endl;
        return static_cast<U>(data) * val;
    }
};

template<typename T>
struct calc_impl_3
{
    template<typename U> static U calc(U val, T data) {
        std::cout << "algo 3, T:" << typeid(T).name() << ", U:" << typeid(U).name() << std::endl;
        return static_cast<U>(data) - val;
    }
};

template<typename T, typename... U>
std::shared_ptr<algo_ifce<U...>> make_algo(calc_enum e, T data)
{
    switch (e) {
    case calc_enum::calc_1: return std::make_shared<algo_ifce<U...>>(std::make_shared<algo_impl<calc_impl_1<T>, T, U...>>(data)); break;
    case calc_enum::calc_2: return std::make_shared<algo_ifce<U...>>(std::make_shared<algo_impl<calc_impl_2<T>, T, U...>>(data)); break;
    case calc_enum::calc_3: return std::make_shared<algo_ifce<U...>>(std::make_shared<algo_impl<calc_impl_3<T>, T, U...>>(data)); break;
    default: throw std::runtime_error("unrecognised algo");
    }
}

template<typename T, typename... U>
struct algo_container
{
    algo_container(calc_enum algo, T data)
        : algo_{ make_algo<T, U...>(algo, data) }
    {}

    template<typename V> V calc(V val) {
        return algo_->template calc_ifce<V>(val);
    }

    std::shared_ptr<algo_ifce<U...>> algo_;
};

template<typename T>
struct my_num
{
    my_num(T t) 
        : t_{ t }
    {}

    operator T() const { return t_; }

    T t_;
};

template<typename T> my_num(T)->my_num<T>;

void test_algo_c()
{
    //               data type        types of calc supported            runtime algo     data
    //             v------------v  v---------------------------v      v---------------v   v--v
    algo_container<double,         double, float, my_num<double> > a1(calc_enum::calc_1,  1.0);
    algo_container<float,          double, float, my_num<double> > a2(calc_enum::calc_2,  1.0f);
    algo_container<my_num<double>, double, float, my_num<double> > a3(calc_enum::calc_3,  1.0);

    a1.calc(2.0f);         // supported                  output: algo 1, T : double, U : float
    a1.calc(3.0);          // supported                  output: algo 1, T : double, U : double
    a1.calc(my_num(-1.0)); // supported                  output: algo 1, T : double, U : struct my_num<double>
    a2.calc(2.0f);         // supported                  output: algo 2, T : float, U : float
    a2.calc(2.0);          // supported                  output: algo 2, T : float, U : double
    a2.calc(my_num(-2.0)); // supported                  output: algo 2, T : float, U : struct my_num<double>
    //a2.calc(5);          // crash, no int version...   output: n/a
    a3.calc(2.0f);         // supported                  output: algo 3, T : struct my_num<double>, U : float
    a3.calc(2.0);          // supported                  output: algo 3, T : struct my_num<double>, U : double
    a3.calc(my_num(1.5));  // supported                  output: algo 3, T : struct my_num<double>, U : struct my_num<double>

}

Обновление

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

#include <memory>
#include <iostream>

enum class calc_enum { calc_1, calc_2, calc_3 };

// interface class
template<typename... U> struct algo_ifce;

template<>
struct algo_ifce<>
{};

template<typename U, typename... REST>
struct algo_ifce<U, REST...> : algo_ifce<REST...>
{
    template<typename V>
    V calc_ifce(V val) {
        if constexpr (std::is_same_v<U, V>) return calc_dispatch(val);
        else return algo_ifce<REST...>::template calc_ifce<V>(val);
    }
    virtual U calc_dispatch(U val) = 0;
};

template<typename F, typename I, typename T, typename... U>
struct algo_impl;

template<typename F, typename I, typename T>
struct algo_impl<F, I, T> : I
{
    algo_impl(T data) : data_{ data } {}
    T data_;
};

template<typename F, typename I, typename T, typename U, typename... REST>
struct algo_impl<F, I, T, U, REST...> : algo_impl<F, I, T, REST...>
{
    algo_impl(T data) : algo_impl<F, I, T, REST...>{ data } {}
    virtual U calc_dispatch(U val) override { return F::template calc<U>(val, algo_impl<F, I, T, REST...>::data_); }
};

// algo types
template<typename T>
struct calc_impl_1
{
    template<typename U> static U calc(U val, T data) {
        std::cout << "algo 1, T:" << typeid(T).name() << ", U:" << typeid(U).name() << std::endl;
        return static_cast<U>(data) + val;
    }
};

template<typename T>
struct calc_impl_2
{
    template<typename U> static U calc(U val, T data) {
        std::cout << "algo 2, T:" << typeid(T).name() << ", U:" << typeid(U).name() << std::endl;
        return static_cast<U>(data) * val;
    }
};

template<typename T>
struct calc_impl_3
{
    template<typename U> static U calc(U val, T data) {
        std::cout << "algo 3, T:" << typeid(T).name() << ", U:" << typeid(U).name() << std::endl;
        return static_cast<U>(data) - val;
    }
};

template<typename T, typename... U>
std::unique_ptr<algo_ifce<U...>> make_algo(calc_enum e, T data)
{
    switch (e) {
    case calc_enum::calc_1: return std::make_unique<algo_impl<calc_impl_1<T>, algo_ifce<U...>, T, U...>>(data); break;
    case calc_enum::calc_2: return std::make_unique<algo_impl<calc_impl_2<T>, algo_ifce<U...>, T, U...>>(data); break;
    case calc_enum::calc_3: return std::make_unique<algo_impl<calc_impl_3<T>, algo_ifce<U...>, T, U...>>(data); break;
    default: throw std::runtime_error("unrecognised algo");
    }
}

template<typename T, typename... U>
struct algo_container
{
    algo_container(calc_enum algo, T data)
        : algo_{ make_algo<T, U...>(algo, data) }
    {}

    template<typename V> V calc(V val) {
        return algo_->template calc_ifce<V>(val);
    }

    std::unique_ptr<algo_ifce<U...>> algo_;
};

template<typename T>
struct my_num
{
    my_num(T t) 
        : t_{ t }
    {}

    operator T() const { return t_; }

    T t_;
};

template<typename T> my_num(T)->my_num<T>;

void test_algo_d()
{
    //               data type        types of calc supported            runtime algo     data
    //             v------------v  v---------------------------v      v---------------v   v--v
    algo_container<double,         double, float, my_num<double> > a1(calc_enum::calc_1,  1.0);
    algo_container<float,          double, float, my_num<double> > a2(calc_enum::calc_2,  1.0f);
    algo_container<my_num<double>, double, float, my_num<double> > a3(calc_enum::calc_3,  1.0);

    a1.calc(2.0f);         // supported                  output: algo 1, T : double, U : float
    a1.calc(3.0);          // supported                  output: algo 1, T : double, U : double
    a1.calc(my_num(-1.0)); // supported                  output: algo 1, T : double, U : struct my_num<double>
    a2.calc(2.0f);         // supported                  output: algo 2, T : float, U : float
    a2.calc(2.0);          // supported                  output: algo 2, T : float, U : double
    a2.calc(my_num(-2.0)); // supported                  output: algo 2, T : float, U : struct my_num<double>
    //a2.calc(5);          // doesn't compile as wanted...
    a3.calc(2.0f);         // supported                  output: algo 3, T : struct my_num<double>, U :float
    a3.calc(2.0);          // supported                  output: algo 3, T : struct my_num<double>, U :double
    a3.calc(my_num(1.5));  // supported                  output: algo 3, T : struct my_num<double>, U :struct my_num<double>

}
...