Привет, я столкнулся со следующей проблемой: у меня есть класс со множеством функций-членов определенного шаблона. Каждая из этих функций-членов имеет три перегрузки, и эти перегрузки одинаковы для всех рассматриваемых функций-членов. Под «одинаковым» я подразумеваю, что они имеют одинаковую подпись, за исключением имени функции. Это приводит к частому повторению кода в состояниях управления, выбирающих нужный метод. Это неприятно поддерживать при добавлении новых методов. Более того, у меня есть класс перечисления (основанный на беззнаковых целых числах) с элементом для каждой из этих функций-членов. Я пытаюсь создать сопоставление класса перечисления с функциями-членами (см. пример ниже).
У меня есть несколько функций-членов memfunc1
, memfunc2
, ... и у меня есть 3 функции (скажем, caller1
, caller2
, caller3
), каждая из которых принимает аргументы одной из трех перегрузок плюс перечисление в качестве аргумента, который сообщает мне, какую из memfuncs вызывать. Я хочу избежать выполнения перечисления сопоставления с memfunc три раза, а не один раз. Я могу себе представить, что это возможно, поскольку компилятор знает сигнатуры, поскольку перегрузки каждой memfunc появляются в отдельных путях управления.
enum class Algos : unsigned int {
FUN1 = 0,
FUN2,
FUN3,
NUM_OF_FUNCTIONS
};
class Algorithms {
Type1 memfunc1();
Type1 memfunc1(const Type2&) const;
Type1 memfunc1(Type3&, const Type2&);
Type1 memfunc2();
Type1 memfunc2(const Type2&) const;
Type1 memfunc2(Type3&, const Type2&);
Type1 memfunc3();
Type1 memfunc3(const Type2&) const;
Type1 memfunc3(Type3&, const Type2&);
};
То, что я сейчас делаю, это
Type1 Algorithms::caller1(Algos algo) {
if (algo == Algos::FUN1) {
return memfunc1();
}
if (algo == Algos::FUN2) {
return memfunc2();
}
if (algo == Algos::FUN3) {
return memfunc3();
}
}
Type1 Algorithms::caller2(const Type2& arg, Algos algo) {
if (algo == Algos::FUN1) {
return memfunc1(arg);
}
if (algo == Algos::FUN2) {
return memfunc2(arg);
}
if (algo == Algos::FUN3) {
return memfunc3(arg);
}
}
Type1 Algorithms::caller3(Type3& arg1, const Type2& arg2, Algos algo) {
if (algo == Algos::FUN1) {
return memfunc1(arg1, arg2);
}
if (algo == Algos::FUN2) {
return memfunc2(arg1, arg2);
}
if (algo == Algos::FUN3) {
return memfunc3(arg1, arg2);
}
}
Но я хочу отделить сопоставление перечислений с функцией-членом от конкретной перегрузки. Потому что все три вызывающие функции делают более или менее одно и то же:
FUN1 -> memfunc1
FUN2 -> memfunc2
FUN3 -> memfunc3
...
таким образом, который позволяет вызывать как:
Type1 a = std::invoke(mapping(FUN1), const Type2&);
Я не привязан именно к этому синтаксису, используя std::invoke
, и также вполне нормально предоставить экземпляр Algorithms
при вызове, но я хочу, чтобы компилятор выбрал правильную перегрузку функции на основе предоставленных аргументов. В идеале отображение может быть определено во время компиляции, поскольку перечисления и предоставленные аргументы известны во время компиляции.
До сих пор я читал некоторые связанные сообщения о переполнении стека, в том числе:
а также многие страницы cppreference, включая https://en.cppreference.com/w/cpp/utility/functional/mem_fn
Мой подход до сих пор заключался в создании массива
using Overload2 = std::function<Type1(const Type2&)>;
std::array<Overload2, std::to_underlying(Algos::NUM_OF_FUNCTIONS)> pMethods;
а затем заполним массив, вызвав
pMethods.at(std::to_underlying(Algos::FUN1)) = [this](const Type2& var) {
return memfunc1(var);
};
и аналогично для других функций-членов и перегрузок. Однако это нежелательное решение, поскольку оно перемещает повторение кода в другую часть кода, но не устраняет его.
От идеи использования std::unordered_map
я отказался на раннем этапе процесса, потому что, насколько мне известно, этот контейнер не очень хорошо работает с ключевым словом constexpr
, поскольку это нелитеральный тип. Кажется, я помню несколько SO-сообщений, посвященных этой проблеме.
Меня устраивает использование последних версий C++, если они компилируются в g++-14 и clang++-18. Я бы предпочел решения, не использующие никаких библиотек, кроме stl, и предпочел бы безопасные для памяти решения stl, а не решения в стиле c.
Заранее спасибо!
На данный момент я слишком новичок на платформе, чтобы иметь возможность голосовать за ваши ответы, но я считаю их все полезными. Не только тот, который я отмечу как принятый.
Есть ли причина, по которой вы хотите сделать это самостоятельно, а не использовать механизм динамической диспетчеризации, встроенный в сам базовый язык C++?
@user12002570 user12002570 Поскольку я прошу разработать код, а не исправить ошибку, минимальный воспроизводимый пример будет достаточным ответом на мой вопрос. Таким образом, у меня нет ни одного. Но я могу предоставить более подробное описание моего варианта использования.
Минимальный воспроизводимый пример предназначен не только для отладки. Это небольшая иллюстрация вашей проблемы, которая работает так же, как и у вас, и мы можем выполнить ее рефакторинг, чтобы протестировать наше решение. В противном случае это превратится в игру в угадайку и перестановку стоек ворот, которая приведет только к потере времени.
@Eljay Нет, нет причин. Если вы видите, как динамическая диспетчеризация решает проблему, поделитесь, как это сделать. Однако мне кажется, что динамическая диспетчеризация не работает во время компиляции.
Вероятно, это можно решить с помощью макросов, но это будет решение в стиле C. Вы определенно хотите сохранить функции-члены и их три варианта в одном классе? В противном случае было бы больше возможностей для решения этой проблемы.
Или иметь в классе четвертый общий вариант шаблонной функции для каждой функции-члена?
... для меня это не похоже на что-то работающее во время компиляции. Я не понимаю, что вы здесь имеете в виду.
@Sebastian «Или у вас есть четвертый общий вариант шаблонной функции в классе для каждой функции-члена?» Звучит чисто. Я был бы признателен за ваш ответ. Перемещение функций-членов из класса Algorithms в производный класс также допустимо.
@Eljay Динамическая отправка происходит во время выполнения согласно определению в Википедии. В моем случае нет необходимости делать это во время выполнения, поскольку вся необходимая информация присутствует во время компиляции.
@user-1 затем дайте вашим функциям разные имена, а не параметр перечисления. Если только вы не хотите выбрать algo
во время выполнения, то есть динамическую отправку.
Ваша функция выполняет динамическую отправку во время выполнения.
Возможно, я не совсем ясно выразился. Отображение желательно фиксировать во время компиляции, но не аргумент, отображаемый программой. И вопрос требует четкого способа записать эту карту.
В функции шаблона вы можете иметь один массив для каждого вызывающего элемента.
class Algorithms {
Type1 memfunc1() { return 1; }
Type1 memfunc1(const Type2&) { return 1; }
Type1 memfunc1(Type3&, const Type2&) { return 1; }
Type1 memfunc2() { return 2; }
Type1 memfunc2(const Type2&) { return 2; }
Type1 memfunc2(Type3&, const Type2&) { return 2; }
Type1 memfunc3() { return 3; }
Type1 memfunc3(const Type2&) { return 3; }
Type1 memfunc3(Type3&, const Type2&) { return 3; }
template<typename... Args>
Type1 caller(Algos algo, Args... args) {
static constexpr std::array<Type1(Algorithms::*)(Args...), std::to_underlying(Algos::NUM_OF_FUNCTIONS)> dispatch = { &Algorithms::memfunc1, &Algorithms::memfunc2, &Algorithms::memfunc3 };
return std::invoke(dispatch[std::to_underlying(algo)], this, args...);
}
public:
Type1 caller1(Algos algo) {
return caller<>(algo);
}
Type1 caller2(const Type2& arg, Algos algo) {
return caller<const Type2&>(algo, arg);
}
Type1 caller3(Type3& arg1, const Type2& arg2, Algos algo) {
return caller<Type3&, const Type2&>(algo, arg1, arg2);
}
};
Эта (отредактированная версия) выглядит красиво. Я перенесу это на свой пример и вернусь с отзывами. Кстати, можно ли заменить Algorithms::*
на выражение std::functional
?
Можно ли адаптировать этот пример к случаю перегрузки функций-членов const
?
@user-1 вам понадобится константная версия caller
, а массив будет Type1(Algorithms::*)(Args...) const
, но в остальном все то же самое
Я бы рекомендовал организовать так:
class Algorithm
{
public:
virtual Type1 memfunc() = 0;
virtual Type1 memfunc(const Type2&) = 0;....
};
class Algorithm1: public Algorithm
{
public:
virtual Type1 memfunc();
virtual Type1 memfunc(const Type2&); ....
};
class Algorithm2: public Algorithm
{
public:
virtual Type1 memfunc();
virtual Type1 memfunc(const Type2&); ....
};
...
...
class Algorithms
{
...
// static just for simplicity, not required really
static Algoritm argorithms[3] = {new Algorithm1(), new Algoritm2(), ...};
//wrap und encapsulate
};
Идея состоит в том, чтобы вызвать вот так
Algos algo = FUN1;
Type1 x = Algoritms::alborithms[algo]->memfunc(...);
Если вы разобьете класс на более мелкие части, представляющие отдельные алгоритмы, то вы сможете использовать std::variant
для большей части механизмов (что дружественно к constexpr) и сохранить локализацию своего собственного переключателя.
class Algorithms;
struct Algo1 {
Type1 memfunc(Algorithms*);
Type1 memfunc(Algorithms*, const Type2&);
Type1 memfunc(Algorithms*, Type3&, const Type2&);
};
struct Algo2 {
Type1 memfunc(Algorithms*);
Type1 memfunc(Algorithms*, const Type2&);
Type1 memfunc(Algorithms*, Type3&, const Type2&);
};
// ...
class Algorithms {
using Impl = std::variant<Algo1, Algo2, ...>;
static constexpr Impl ImplForAlgo(Algos a) {
switch(a) {
case Algos::FUN1: return Impl(std::in_place_index<0>);
case Algos::FUN2: return Impl(std::in_place_index<1>);
case Algos::FUN3: return Impl(std::in_place_index<2>);
}
throw std::bad_variant_access();
}
public:
Type1 caller1(Algos algo) {
return std::visit<Type1>([this](auto&& a){
return a.memfunc(this);
}, ImplForAlog(algo))
}
Type1 caller2(const Type2& arg, Algos algo) {
return std::visit<Type1>([this, &arg](auto&& a){
return a.memfunc(this, arg);
}, ImplForAlog(algo))
}
Type1 caller3(Type3& arg1, const Type2& arg2, Algos algo) {
return std::visit<Type1>([this, &arg1, &arg2](auto&& a){
return a.memfunc(this, arg1, arg2);
}, ImplForAlog(algo))
}
};
Я передал Algorithms*
явно на случай, если в отдельных алгоритмах понадобится какое-то состояние из старого монолита. Но если нет, то полученный код будет еще более кратким.
template<auto x>
using val_t = std::integral_constant<decltype(x), x>;
template<auto x>
constexpr val_t<x> val_k = {};
это ценности как типы и моногосударства. Это полезно тем, что с его помощью вы можете конвертировать перечисления в варианты.
template<auto...Xs>
using venum_t = std::variant< val_t<Xs>... >;
a venum_t
— это вариант списка значений в качестве моногосударства.
using vAlgos = venum_t<Algos::FUN1, Algos::FUN2, Algos::FUN3>;
vAlgos
— это оболочка венума вокруг перечисления Algos
. Мы можем автоматизировать его генерацию (используя Algos::NUM_OF_FUNCTIONS
и предполагая смежность).
Во время выполнения vAlgos
является целым числом, значение которого соответствует значению времени выполнения перечисления Algos
(0, 1, 2). Но мы можем использовать std::apply
, чтобы получить значение как константу времени компиляции! Более того, мы можем преобразовать значения времени выполнения в значения времени компиляции следующим образом:
template<class E, E... Xs, class R=venum_t<Xs...> >
R make_venum( E x ) {
using f_t = R(*)();
f_t table[] = {
+[]()->R {
return R(std::in_place_type_t<val_t<Xs>>{});
}...
};
return table[ std::underlying_type_t<E>(x) ]();
}
сейчас
vAlgos make_vAlgos( Algos a ) {
return make_venum<Algos, Algos::FUN1, Algos::FUN2, Algos::FUN3>( a );
}
принимает значение времени выполнения Algos
и создает variant
моносостояния времени компиляции. (Написание этой функции также можно автоматизировать, я пишу ее вручную, чтобы было проще).
Что мы получаем от всего этого шума?
template<Algos algo, class...Ts>
auto memfunc_caller(val_t<algo>, Ts&&...ts) {
if constexpr (algo == Algos::FUN1) {
return memfun1(std::forward<Ts>(ts)...);
} else if constexpr (algo == Algos::FUN2) {
return memfun2(std::forward<Ts>(ts)...);
} else if constexpr (algo == Algos::FUN3) {
return memfun3(std::forward<Ts>(ts)...);
}
};
мы тогда делаем
template<class...Ts>
Type1 Algorithms::caller( vAlgos valgo, Ts&&... ts ) {
return std::visit([&](auto algo){
return memfunc_caller(algo, std::forward<Ts>(ts)...);
}, valgo);
}
template<class...Ts>
Type1 Algorithms::caller( Algos algos, Ts&&... ts ) {
return caller( make_vAlgos(algos), std::forward<Ts>(ts)... );
}
обратите внимание, что передача vAlgos
вместо Algos
экономит часть работы по преобразованию.
Мы можем упростить это до 1 или 2 функций, если вам не нужна вся мощь вариантов над перечислениями.
Минимально воспроизводимый пример был бы хорош.