大家好,我面临以下问题:我有一个类,其中包含许多具有特定模式的成员函数。每个成员函数都有 3 个重载,并且这些重载对于所有考虑的成员函数都是相同的。“相同”是指它们除了函数名称外具有相同的签名。这导致在选择所需方法的控制状态下出现大量代码重复。当添加新方法时,这很难维护。此外,我有一个枚举类(基于无符号整数),每个成员函数都有一个项目。我尝试创建从枚举类到成员函数的映射(参见下面的示例)。

例子

我有几个成员函数memfunc1memfunc2、 … 并且我有 3 个函数(例如caller1caller2caller3),每个函数都采用三个重载之一的参数加上一个枚举作为参数,该参数告诉我要调用哪个 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

  • 一个就很好了。


    – 

  • 您为什么要自己做这件事,而不是使用核心 C++ 语言内置的动态调度机制?


    – 

  • @user12002570 由于我要求的是代码设计而不是错误修复,因此一个最小的可重现示例就足以回答我的问题。因此我没有答案。但我可以提供更详细的用例描述。


    – 

  • 并非仅用于调试。它是对您问题的一个小例子,它对您来说就是如此,我们可以重构以测试我们的解决方案。否则,它将成为一场猜测和转移目标的游戏,只会浪费大量时间。


    – 

  • @Eljay 没有理由。如果您知道动态调度如何解决问题,欢迎分享。但是对我来说,动态调度听起来不像是在编译时工作。


    – 


最佳答案
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*明确地传递了这一点,以防个别算法需要旧整体中的任何状态。但如果不需要,生成的代码会更加简洁。