Сопоставление класса перечисления с перегрузкой функции

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

Пример

У меня есть несколько функций-членов 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.

Заранее спасибо!

Редактировать

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

Минимально воспроизводимый пример был бы хорош.

user12002570 20.08.2024 13:59

Есть ли причина, по которой вы хотите сделать это самостоятельно, а не использовать механизм динамической диспетчеризации, встроенный в сам базовый язык C++?

Eljay 20.08.2024 13:59

@user12002570 user12002570 Поскольку я прошу разработать код, а не исправить ошибку, минимальный воспроизводимый пример будет достаточным ответом на мой вопрос. Таким образом, у меня нет ни одного. Но я могу предоставить более подробное описание моего варианта использования.

user-1 20.08.2024 14:12

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

StoryTeller - Unslander Monica 20.08.2024 14:21

@Eljay Нет, нет причин. Если вы видите, как динамическая диспетчеризация решает проблему, поделитесь, как это сделать. Однако мне кажется, что динамическая диспетчеризация не работает во время компиляции.

user-1 20.08.2024 14:29

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

Sebastian 20.08.2024 14:30

Или иметь в классе четвертый общий вариант шаблонной функции для каждой функции-члена?

Sebastian 20.08.2024 14:48

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

Eljay 20.08.2024 15:15

@Sebastian «Или у вас есть четвертый общий вариант шаблонной функции в классе для каждой функции-члена?» Звучит чисто. Я был бы признателен за ваш ответ. Перемещение функций-членов из класса Algorithms в производный класс также допустимо.

user-1 20.08.2024 15:24

@Eljay Динамическая отправка происходит во время выполнения согласно определению в Википедии. В моем случае нет необходимости делать это во время выполнения, поскольку вся необходимая информация присутствует во время компиляции.

user-1 20.08.2024 15:26

@user-1 затем дайте вашим функциям разные имена, а не параметр перечисления. Если только вы не хотите выбрать algo во время выполнения, то есть динамическую отправку.

Caleth 20.08.2024 15:38

Ваша функция выполняет динамическую отправку во время выполнения.

Eljay 20.08.2024 15:43

Возможно, я не совсем ясно выразился. Отображение желательно фиксировать во время компиляции, но не аргумент, отображаемый программой. И вопрос требует четкого способа записать эту карту.

user-1 20.08.2024 15:58
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
13
80
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Ответ принят как подходящий

В функции шаблона вы можете иметь один массив для каждого вызывающего элемента.

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?

user-1 20.08.2024 15:38

Можно ли адаптировать этот пример к случаю перегрузки функций-членов const?

user-1 20.08.2024 16:25

@user-1 вам понадобится константная версия caller, а массив будет Type1(Algorithms::*)(Args...) const, но в остальном все то же самое

Caleth 20.08.2024 16:31

Я бы рекомендовал организовать так:

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 функций, если вам не нужна вся мощь вариантов над перечислениями.

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

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

Почему синтаксис C# обрабатывает ptr = null иначе, чем разыменование указателя в C++?
Что происходит в C++, когда rvalue типа, доступного только для перемещения, передается функции по значению?
Можно ли вернуть переменную и обновить ее одним оператором вместо того, чтобы сначала копировать переменную?
Вывести класс шаблона с уменьшенным количеством параметров шаблона
C++: Что означает (-1) в конце списка параметров этой функции?
Функция Sony Camera Remote SDK Connect() не возвращает дескриптор устройства
«Ошибка файловой системы: невозможно увеличить рекурсивный итератор каталога: разрешение отклонено» в каталоге 0777
Влияет ли знак на точность и точность чисел с плавающей запятой?
Я пытался создать общие библиотеки C++ для Python, используя cmake и pybind11, но получаю ошибки, которые не знаю, как исправить
Оптимизация компилятора приводит к неверным результатам