Я занимаюсь рефакторингом 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(), но кодовая база — это максимум спагетти, и это компромисс)
«Перегрузка функций с помощью фиктивных структур». Это называется диспетчеризацией тегов.
Диспетчеризация тегов может выполняться в конструкторах, но не в специализации шаблона.
[Стиль] Применительно к методу классов шаблонов вам, возможно, придется добавить дополнительный template
в месте вызова (т. е. this->template baz<option>();
).
Как насчет вызова разных функций, которые делают разные вещи, под разными именами? Какая новая концепция!
В диспетчеризации тегов нет ничего плохого, поэтому в основном вы меняете стиль.
Однако в текущей реализации тег наследуется от базового класса без видимой причины.
У вас может быть непосредственно:
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)
{
//...
}
У меня есть предпочтения в отношении стиля отправки тегов.
Самая большая проблема в вашем коде - это повторяющаяся отправка, которая подвержена ошибкам в случае добавления нового значения перечисления.
Да, это выглядит как хорошая идея.
baz
следует использоватьswitch
без веткиdefault:
, чтобы ваш компилятор предупреждал, если вы забудете перечисление (GCC и Clang предупреждают, не уверен насчет MSVC). Вы даже можете сделатьbaz
универсальным, чтобы вызывать любые функции со значением перечисления, преобразованным в constexpr.