Перегрузка функций с помощью фиктивных структур и специализация шаблонов

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

У нас есть класс перечисления, который часто используется для решения того, как обрабатывать какое-либо значение:

enum class OptionEnum : int {
    optionA,
    optionB
};

void foo( const int val, const OptionEnum option ){
    if ( option == OptionEnum::optionA ){
        doSomething( val );
    } else if ( option == OptionEnum::optionB ) {
        doSomethingElse( val );
    } else {
        throw std::invalid_argument( "Unsupported Option" );
    }

    doMoreStuff();
}

Однако, когда функция особенно сложна, текущий код использует перегрузку и некоторые фиктивные структуры для получения правильного разрешения перегрузки:

struct OptionsStruct{};
static constexpr struct OptionStructA : OptionsStruct{} optionA;
static constexpr struct OptionStructB : OptionsStruct{} optionB;

void foo( const int val, const OptionStructA & ){
    doSomething( val );
}

void foo( const int val, const OptionStructB & ){
    doSomethingElse( val );
}

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

template < typename OptionStructAorB >
void bar( const int val, const OptionStructAorB option ){
    int result = processVal( val );
    foo( result, option );
}

void baz( const int val, const OptionEnum option ){
    if ( option == OptionEnum::optionA ){
        bar( val, optionA );
    } else if ( option == OptionEnum::optionB ) {
        bar( val, optionB );
    } else {
        throw std::invalid_argument( "Unsupported Option in baz" );
    }
}

int main(int argc, char* argv[]) {
    const int inputVal = std::atoi(argv[1]);
    const std::string optionString = argv[2];

    OptionEnum inputOption;
    if ( optionString == "a" ){
        inputOption = OptionEnum::optionA;
    } else if ( optionString == "b" ) {
        inputOption = OptionEnum::optionB;
    } else {
        std::cout << "Invalid option " << optionString << std::endl;
        return 1;
    }

    baz(inputVal, inputOption);

    return 0;
}

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

template< OptionEnum option >
void foo(const int val);

template<>
void foo< OptionEnum::optionA >(const int val){
    doSomething(val);
}

template<>
void foo< OptionEnum::optionB >(const int val){
    doSomethingElse(val);
}

template< OptionEnum option >
void bar(const int val){
    int result = processVal(val);
    foo<option>(result);
}

void baz( const int val, const OptionEnum option ){
    if ( option == OptionEnum::optionA ){
        bar<OptionEnum::optionA>( val );
    } else if ( option == OptionEnum::optionB ) {
        bar<OptionEnum::optionB>( val );
    } else {
        throw std::invalid_argument( "Unsupported Option in baz" );
    }
}

Помимо упрямых опасений по поводу стиля и читаемости, есть ли какие-либо конкретные преимущества у исходного подхода с фиктивными структурами по сравнению с подходом со специализацией шаблонов с точки зрения производительности, безопасности типов и удобства сопровождения (специфично для C++17)? Могут ли возникнуть проблемы с удобством обслуживания при добавлении нового значения перечисления, которое, например, хуже в новом подходе?

(Да, foo(), вероятно, вообще не следует перегружать, и нам следует использовать только fooA() и fooB(), но кодовая база — это максимум спагетти, и это компромисс)

Да, это выглядит как хорошая идея. baz следует использовать switch без ветки default:, чтобы ваш компилятор предупреждал, если вы забудете перечисление (GCC и Clang предупреждают, не уверен насчет MSVC). Вы даже можете сделать baz универсальным, чтобы вызывать любые функции со значением перечисления, преобразованным в constexpr.

HolyBlackCat 11.07.2024 14:14

«Перегрузка функций с помощью фиктивных структур». Это называется диспетчеризацией тегов.

Jarod42 11.07.2024 14:45
std::integral_constant может быть лучше, чем выделенная фиктивная структура (кстати, с ненужным наследованием)
Jarod42 11.07.2024 14:47

Диспетчеризация тегов может выполняться в конструкторах, но не в специализации шаблона.

Jarod42 11.07.2024 14:53

[Стиль] Применительно к методу классов шаблонов вам, возможно, придется добавить дополнительный template в месте вызова (т. е. this->template baz<option>();).

Jarod42 11.07.2024 14:56

Как насчет вызова разных функций, которые делают разные вещи, под разными именами? Какая новая концепция!

n. m. could be an AI 14.07.2024 10:21
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
6
73
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

В диспетчеризации тегов нет ничего плохого, поэтому в основном вы меняете стиль.

Однако в текущей реализации тег наследуется от базового класса без видимой причины.

У вас может быть непосредственно:

enum class OptionEnum : int {
    optionA,
    optionB
};

static constexpr std::integral_constant<OptionEnum::optionA> optionA;
static constexpr std::integral_constant<OptionEnum::optionB> optionB;

Могут ли возникнуть проблемы с удобством обслуживания при добавлении нового значения перечисления?

При использовании switch компилятор может предупредить об отсутствии case. и преобразование времени выполнения в значение времени компиляции выполняется перед использованием, поэтому в этом отношении никто не лучше другого.

Использование std::variant с std::visit может позволить выполнить переключение только один раз:

std::variant<
    std::integral_constant<OptionEnum, OptionEnum::optionA>,
    std::integral_constant<OptionEnum, OptionEnum::optionB>
> toVariant(OptionEnum option)
{
    switch (option) {
        case OptionEnum::optionA: return std::integral_constant<OptionEnum, OptionEnum::optionA>{};
        case OptionEnum::optionB: return std::integral_constant<OptionEnum, OptionEnum::optionB>{};
    }
    std::unreachable(); // or throw
}

а потом

std::visit(
    [&](auto option){ // or [&]<OptionEnum E>(std::integral_constant<OptionEnum, E>)
        foo(val, option); // tag dispatching
        bar<option()>(val); // template
    }, runtime_option);

Есть ли какие-либо конкретные преимущества с точки зрения производительности, безопасности типов и удобства сопровождения (специфично для C++17)?

  • Диспетчеризация тегов добавляет дополнительный пустой параметр, который, вероятно, следует оптимизировать.

  • Сообщения об ошибках могут отличаться при неправильном использовании:

  • отсутствие перегрузки (для отправки тегов) приведет к ошибке компиляции с несоответствием аргумента.

  • отсутствие специализации приведет к ошибке компоновщика или потребует = delete основного шаблона.

Есть ли какие-либо конкретные преимущества у диспетчеризации тегов?

Вы не можете частично специализировать функции,

Таким образом, вы не можете иметь

template <OptionEnum, typename T> void baz(T);

template <typename T> void baz<OptionEnum::optionA>(T); // Not possible

тогда вам нужно создать функтор

template <OptionEnum>
struct baz_impl;

template <>
struct baz_impl<OptionEnum::optionA>
{
    template <typename T>
    void operator() (T) const { /* .. */ }
};
template <>
struct baz_impl<OptionEnum::optionB>
{
    template <typename T>
    void operator() (T) const { /* .. */ }
};

template <OptionEnum option, typename T>
void baz(T t) { baz_impl<option>{}(t); }

но с отправкой тегов вы можете просто:

template <typename T>
void baz(std::integral_constant<OptionEnum, OptionEnum::optionA>, T)
{
    //...
}
template <typename T>
void baz(std::integral_constant<OptionEnum, OptionEnum::optionB>, T)
{
    //...
}

У меня есть предпочтения в отношении стиля отправки тегов.

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

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