大家好,我面临以下问题:我有一个类,其中包含许多具有特定模式的成员函数。每个成员函数都有 3 个重载,并且这些重载对于所有考虑的成员函数都是相同的。“相同”是指它们除了函数名称外具有相同的签名。这导致在选择所需方法的控制状态下出现大量代码重复。当添加新方法时,这很难维护。此外,我有一个枚举类(基于无符号整数),每个成员函数都有一个项目。我尝试创建从枚举类到成员函数的映射(参见下面的示例)。
例子
我有几个成员函数memfunc1
、memfunc2
、 … 并且我有 3 个函数(例如caller1
、caller2
、caller3
),每个函数都采用三个重载之一的参数加上一个枚举作为参数,该参数告诉我要调用哪个 memfunc。我想避免将枚举映射到 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
,但我希望编译器根据提供的参数选择正确的函数重载。理想情况下,映射可以在编译时定义,因为枚举和提供的参数在编译时是已知的。
到目前为止,我阅读了一些相关的 Stack Overflow 帖子,包括:
以及许多 cppreference 页面,包括
到目前为止,我的方法是创建一个数组
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
在流程早期放弃了使用 I 的想法,因为据我所知,这个容器与关键字配合得不好,constexpr
因为它是非文字类型。我想我记得有几篇 SO 帖子在解决这个问题。
只要 c++ 的最新版本可以在 g++-14 和 clang++-18 中编译,对我来说使用它们没问题。我更喜欢不使用除 stl 之外的其他库的解决方案,并且更喜欢内存安全的 stl 解决方案,而不是 c 风格的解决方案。
提前致谢!
编辑
目前,我在这个平台上还太新,无法对您的答案点赞,但我认为它们都很有帮助。不仅仅是我会标记为已接受的答案。
13
最佳答案
4
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>... >;
avenum_t
是单态值列表的变体。
using vAlgos = venum_t<Algos::FUN1, Algos::FUN2, Algos::FUN3>;
vAlgos
是枚举的 venum 包装器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 个函数。
|
我建议像这样组织:
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(...);
|
在模板函数中,您可以为每个调用者成员设置一个数组。
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);
}
};
3
-
这个(编辑版本)看起来不错。我会将其转移到我的示例中,并会提供反馈。顺便问一下,可以用表达式替换
Algorithms::*
吗std::functional
?
–
-
是否可以将该示例适应于成员函数过载的情况
const
?
– -
@user-1 您需要一个 const 限定版本
caller
,并且数组将是Type1(Algorithms::*)(Args...) const
,但其他方面都相同
–
|
如果将类分解为更小的部分,代表各个算法,那么您可以利用大部分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*
明确地传递了这一点,以防个别算法需要旧整体中的任何状态。但如果不需要,生成的代码会更加简洁。
|
–
–
–
–
–
|