使用指向成员的指针数组还是使用开关更好

Is it better to use an array of pointers to members or a switch?

本文关键字:开关 更好 数组 成员 指针      更新时间:2023-10-16

在我的学校里,我们强烈鼓励使用指向成员的指针数组,而不是在C++(和C)中切换(或多个else if)。

由于我认为使用这样的数组(我实际上使用指向成员的指针映射)而不是switch语句没有任何意义,我想知道是否真的有任何优化可以推荐指向函数的指针。

以下是让我认为最好使用switch:的原因

  • 指向成员的指针数组(尤其是映射)内存很大(std::string作为键,指针作为值),需要存储在类中(没有任何意义,因为它不是对象属性…),或者每次使用它们在函数中重新创建(如果静态声明):

    std::map<std::string, void (MyClass::*)(...)>   operations;
    
  • 它们很难实例化并准备使用:

    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("push", &Parser::push));
    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("pop", &Parser::pop));
    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("dump", &Parser::dump));
    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("assert", &Parser::assert));
    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("add", &Parser::add));
    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("sub", &Parser::sub));
    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("mul", &Parser::mul));
    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("div", &Parser::div));
    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("mod", &Parser::pop));
    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("print", &Parser::print));
    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("exit", &Parser::exit));
    
  • 它迫使您在某些函数中使用无用的参数,并使用可能是const的非常数成员。例如,在我之前的一段代码中,如果映射中没有使用"print"answers"assert",它们可能是const,而且大多数函数都没有使用参数,但"push"answers"断言"是。。。

  • 你必须验证你想要使用的指针是否存在于映射中,而不是让"默认"情况来处理它,并且调用很难读取:

    if (operations.find(myOperation) != operations.end())
    (this->*(operations.find(myOperation)->second))(myParameter);
    

那么,为什么我们被迫使用指向成员的指针,而不仅仅是一个clear switch语句,甚至是else if?

谢谢。

这取决于情况。具有多个未连接选项的切换情况实际上与大if-else-慢速相同。好的优化是使用偏移表(或跳转表)执行所需的操作,建议您执行该操作。

奇怪的是,如果switch-case写得好,编译器通常可以自动执行这种优化。

但是写得好意味着什么?

这意味着,您必须设计条目索引,这样就可以轻松快速地计算需要执行的条目的位置。考虑以下代码:

int n = 0;
std::cin >> n;
if(n == 1) printf("1n");
else if(n == 2) printf("2n");
else if(n == 3) printf("3n");
else if(n == 4) printf("4n");

这是可能的输出(VC11上的实际输出,用/O2编译):

011AA799  mov         eax,dword ptr [n]  
011AA79C  cmp         eax,1 //is n equal to 1?
011AA79F  jne         main+34h (011AA7B4h) //if yes, continue, if not, jump... [J1]
011AA7A1  push        1262658h  
011AA7A6  call        printf (011E1540h) // print 1
011AA7AB  add         esp,4  
011AA7AE  xor         eax,eax  
011AA7B0  mov         esp,ebp  
011AA7B2  pop         ebp  
011AA7B3  ret  
011AA7B4  cmp         eax,2 // [J1] ...here. Is n equal to 2?
011AA7B7  jne         main+4Ch (011AA7CCh) //If yes, continue, if not, jump... [J2]
011AA7B9  push        126265Ch  
011AA7BE  call        printf (011E1540h) // print 2
011AA7C3  add         esp,4  
011AA7C6  xor         eax,eax  
011AA7C8  mov         esp,ebp  
011AA7CA  pop         ebp  
011AA7CB  ret  
011AA7CC  cmp         eax,3 // [J2] ...here. Is n equal to 3? (and so on...)
011AA7CF  jne         main+64h (011AA7E4h)  
011AA7D1  push        1262660h  
011AA7D6  call        printf (011E1540h)
[...]

基本上是if-else。现在,让我们更改代码:

int n = 0;
std::cin >> n;
switch(n)
{
case  1: printf("1n"); break;
case  2: printf("2n"); break;
case  3: printf("3n"); break;
case  4: printf("4n"); break;
}

可能输出:

011BA799  mov         eax,dword ptr [n]  // switch case will run if n is 1-4
011BA79C  dec         eax //decrement by one, now it should be in 0-3
011BA79D  cmp         eax,3 // compare with 3
011BA7A0  ja          $LN4+46h (011BA7EFh) //if greater than 3, skip switch
011BA7A2  jmp         dword ptr [eax*4+11BA7F8h] //otherwise compute offset of instrcution and jump there

我没有发布对printf的调用——本质上是一样的,但没有任何cmp或跳转指令。

当然,这个输出只是众多可能的输出之一,但关键是:设计良好的应用程序,对条件部分进行智能优化,可以执行更高效的。在这里,编译器能够直接跳转到正确的指令,因为它可以很容易地计算它的偏移量——所有情况都用数字标记,数字增长一。

为了更直接地回答你的问题:给你的建议在技术上是正确的,但我会专注于编译器友好的优化,而不是复杂的代码(这可能会也可能不会显著提高速度),这是每个人都能理解和依赖的(只要编译器足够聪明,能够利用这一优势生成优化的代码)。

与切换指令相比,您对指向成员函数的指针数组的优缺点的分析已经非常好了。

但这一切都取决于上下文:

  • 当然,从技术上讲,你是完全正确的:如果你只想更换一个交换机,这样的阵列会非常麻烦。更不用说编译器,他们可以通过使用比数组少使用一个间接寻址的跳表来优化开关。

  • 但是您的示例代码实现了一种命令设计模式。从设计的角度来看,这可能在进化性和可维护性方面有很大的优势,超过了技术缺陷。例如,它可以很容易地在应用程序中用于实现撤消/重做功能。它还简化了几个同时使用的用户界面允许在对象上触发这些命令的情况(例如:命令行窗口和GUI)

上下文很重要。

如果你用电脑工作,我认为更喜欢数组,因为与非常快速的比较相比,获得结果非常快,这是用内存支付的。这在内存方面很昂贵,但在阵列非常大的情况下速度很快。

如果上下文是一个微控制器,那么内存非常昂贵,并且不能浪费来保存所有数组。尤其是在几乎不使用阵列的情况下。但是,由于不使用内存,而且汇编程序在微控制器中速度非常快,因此可以首选开关。

  • 如果你有这么多内存,并且是一种高级编程语言,那么数组可能会更好
  • 如果您的内存太少,并且使用汇编程序或微型计算机中的C等低级编程语言,那么最好使用switch(或rom表)