Boost :: Polycollection, STD :: вариант или CRTP?

Предположим, что «стандартная» парадигма наследования C ++:

struct GeneralFunc
{
/*..members..*/
virtual double value(double a, double b) { return 0; }
};

struct Func_classA : GeneralFunc
{
/*..members..*/
double value(double a, double b) { return a * b; }
};

struct Func_classB : GeneralFunc
{
/*..members..*/
double value(double a, double b) { return a + b; }
};

void main(){
double a = 1.0, b = 1.0;
std::vector<GeneralFunc*> my_functions;
//fill my_functions from input
for (auto& f : my_functions)
{
double v = f->value(a, b);
}
}

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

доступно несколько вариантов:

повышение :: polycollection

#include <boost/poly_collection/base_collection.hpp>
//...rest the same
boost::base_collection<GeneralFunc> my_functions
//...rest the same

станд :: вариант

#include <variant>
//...rts
using funcs = std::variant<Func_classA, Func_classB /*..possibly more../*>
std::vector<funcs> my_functions

или CRTP (Любопытно повторяющийся шаблон)
Дайте мне знать правильную номенклатуру для этого, но здесь я «выкинул» базовый класс, основанный на «типе» — своего рода ручная рассылка.

template<typename T>
struct GeneralFunc
{
/*..members..*/
int my_type;
double value(double a, double b) {
switch (my_type){
case TYPE_A:
return static_cast<Func_classA*>(this)->value(a,b);
/*..you get the idea..*/

Я в порядке, жертвуя предельной эффективностью для простоты разработки, но есть ли консенсус по «наилучшей практике» в этом случае?

РЕДАКТИРОВАТЬ * исправлены некоторые опечатки; мое текущее развитие «в разработке» CRTP последний вариант.

РЕШЕНИЕ:

После тестирования оба Boost :: Polycollection и Std :: Вариант являются действительными подходами. Тем не менее, это оказалось гораздо более эффективным (по памяти, может быть немного отключен).

enum ftype { A = 0, B, C };
struct GeneralFunc
{
ftype my_type;
GeneralFunc(ftype t) : my_type(t) {}
inline double value(double a, double b) const; // delay definition until derived classes are defined
}

struct Func_classA : GeneralFunc
{
Func_classA() : GeneralFunc(ftype::A) {}
inline double value(double a, double b) const { return a * b; }
}
/* define B, C (& whatever) */

inline double GeneralFunc::value(double a, double b)
{
switch(my_type){
case (ftype::A):
return static_cast<Func_classA*>(this)->value(a,b);
/* same pattern for B, C, ect */
}
}

void main(){
std::vector<std::unique_ptr<GeneralFunc>> funcs;
funcs.push_back(std::make_unique<Func_classA>());
funcs.push_back(std::make_unique<Func_classB>());

funcs[0]->value(1.0,1.0); // calls Func_classA.value
funcs[1]->value(1.0,1.0); // calls Func_classB.value
}

2

Решение

Я хотел бы просто использовать std::function в качестве контейнера, а не переписывать его.

using GeneralFunc = std::function<double(double, double);

struct Func_classA
{
/*..members..*/
double value(double a, double b) { return a * b; }
/*explicit*/ operator GeneralFunc () const { return [this](double a, double b){ value(a, b) }; }
};

struct Func_classB
{
/*..members..*/
double value(double a, double b) { return a + b; }
/*explicit*/ operator GeneralFunc () const { return [this](double a, double b){ value(a, b) }; }
};

void main(){
double a = 1.0, b = 1.0;
std::vector<GeneralFunc> my_functions;
//fill my_functions from input
for (auto& f : my_functions)
{
double v = f(a, b);
}
}
1

Другие решения

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

#include <tuple>
#include <functional>

template<int ... Id, typename Functions>
auto apply(std::integer_sequence<int, Id ...>, Functions& my_functions, double& v, double a, double b){
([](auto a, auto b){a=b;}(v, std::get<Id>(my_functions)( a, b )), ...);
}

int main(){
auto fA = [](double a, double b){return a*b;};
auto fB = [](double a, double b){return a+b;};
//create the tuple
auto my_functions=std::make_tuple(fA, fB);
double v=0;
double a = 1.;
double b = 1.;
//iterate over the tuple
apply(std::make_integer_sequence<int, 2>(), my_functions, v, a, b);

}
Таким образом, вы создаете типобезопасную абстракцию с нулевыми накладными расходами, поскольку компилятор знает все о используемых вами типах (вам не нужен механизм стирания типов). Также нет необходимости в виртуальных функциях (как в CRTP), так что компилятор, вероятно, встроит вызовы функций. В приведенном выше фрагменте используются общие лямбда-выражения C ++ 17, которые также могут быть реализованы в соответствии с C ++ 14 или C ++ 11, но они будут более многословными. Я бы предпочел это, а не CRTP, потому что для меня это выглядит более читабельно: нет статического приведения к производному классу и нет искусственной иерархии наследования.

РЕДАКТИРОВАТЬ: из вашего ответа кажется, что вам не нужен CRTP здесь, то, что вы пишете с использованием решения CRTP, эквивалентно этому

enum ftype { A = 0, B, C };

auto fA = [](double a, double b){return a*b;};
auto fB = [](double a, double b){return a+b;};

int main(){

std::vector<ftype> types(2);
types[0]=A;
types[1]=B;

auto value = [&types](double a, double b, ftype i){
switch(i){
case (ftype::A):
return fA(a,b);
break;
case (ftype::B):
return fB(a,b);
break;
}
};

double v=value(1., 1., A);
v=value(1., 1., B);

}

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

0

По вопросам рекламы [email protected]