使用指向成员的指针数组或开关更好吗?
Is it better to use an array of pointers to members or a switch?
在我的学校,我们强烈鼓励在 C++(和 C)中使用指向成员的指针数组而不是 switch(或多个 else if)。
由于我没有看到使用此类数组(我实际上使用指向成员的指针映射)而不是 switch 语句的任何意义,我想知道是否真的有任何类型的优化可以推荐指针功能。
以下是让我认为使用 switch 会更好的原因:
指向成员的指针的数组(尤其是映射)占用大量内存(std::string 作为键,指针作为值)并且需要存储在 class (没有任何意义,因为它不是对象 属性...)或者如果静态声明则每次在函数中使用它们重新创建:
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" 和 "assert" 如果未在映射中使用,则可能是 const,并且大多数函数不使用参数,但 "push" 和 "assert" 是...
你必须验证你要使用的指针存在于地图中,而不是让 "default" 案例处理它,并且调用很难阅读:
if (operations.find(myOperation) != operations.end())
(this->*(operations.find(myOperation)->second))(myParameter);
那么,为什么我们被迫使用指向成员的指针,而不仅仅是一个清晰的 switch 语句,甚至是 else-ifs?
谢谢。
你对成员函数指针数组与switch指令优缺点的分析已经很不错了。
但这完全取决于上下文:
当然从技术上讲,你是完全正确的:如果你只是想更换一个开关,这样的阵列是非常麻烦的。更不用说可以使用 jump tables 优化开关的编译器,它使用的间接寻址比您的数组少一个。
但是您的示例代码实现了一种command design pattern。从设计的角度来看,这在进化性和可维护性方面可能具有重要的优势,这些优势超过了技术缺陷。例如,它可以很容易地用在应用程序中以实现 undo/redo 功能。它还简化了多个同时用户界面允许在对象上触发这些命令的情况(例如:命令行 window 和 GUI)
视情况而定。具有多个 未连接 选择的 Switch-case 实际上与大 if-else
相同 - 慢。好的优化是使用偏移table(或跳转table)执行所需的操作,建议您实现。
奇怪的是,编译器通常可以自动执行这种优化 - 如果 switch-case
写得好。
但是写得好是什么意思?
这意味着,你必须设计条目索引,这样才能轻松快速地计算出需要执行的条目的位置。考虑以下代码:
int n = 0;
std::cin >> n;
if(n == 1) printf("1\n");
else if(n == 2) printf("2\n");
else if(n == 3) printf("3\n");
else if(n == 4) printf("4\n");
这是可能的输出(实际输出,在 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("1\n"); break;
case 2: printf("2\n"); break;
case 3: printf("3\n"); break;
case 4: printf("4\n"); 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
我没有 post 调用 printf
- 基本相同,但没有任何 cmp
或跳转指令。
这个输出当然只是许多可能的输出之一,但重点是:设计良好的应用程序在条件部分进行智能优化,可以执行得更高效。在这里,编译器能够直接跳转到正确的指令,因为可以很容易地计算出它的偏移量——所有情况都用数字标记,数字增加一。
为了更直接地回答你的问题:你得到的建议在技术上是正确的,但不是复杂的代码(可能会或可能不会显着提高速度),我会比每个人都更关注编译器友好的优化可以理解和依赖(只要编译器足够聪明,可以利用这一优势并生成优化代码)。
上下文很重要。
如果您使用 PC,我认为更喜欢阵列,因为与非常比较相比,获得结果的速度非常快,购买您的内存。
这在内存中很昂贵,但对于非常大的数组来说速度很快。
如果context是单片机,内存很贵,不能浪费去存所有数组。特别是如果阵列几乎不被使用。
但是可以优先选择开关,因为不使用内存,而且微控制器中的汇编程序非常快。
- 如果你有那么多内存和一门高级编程语言,也许数组更好。
- 如果你的内存很少,并且使用汇编程序或微型 C 等低级编程语言,最好是开关(或 rom 表)
在我的学校,我们强烈鼓励在 C++(和 C)中使用指向成员的指针数组而不是 switch(或多个 else if)。
由于我没有看到使用此类数组(我实际上使用指向成员的指针映射)而不是 switch 语句的任何意义,我想知道是否真的有任何类型的优化可以推荐指针功能。
以下是让我认为使用 switch 会更好的原因:
指向成员的指针的数组(尤其是映射)占用大量内存(std::string 作为键,指针作为值)并且需要存储在 class (没有任何意义,因为它不是对象 属性...)或者如果静态声明则每次在函数中使用它们重新创建:
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" 和 "assert" 如果未在映射中使用,则可能是 const,并且大多数函数不使用参数,但 "push" 和 "assert" 是...
你必须验证你要使用的指针存在于地图中,而不是让 "default" 案例处理它,并且调用很难阅读:
if (operations.find(myOperation) != operations.end()) (this->*(operations.find(myOperation)->second))(myParameter);
那么,为什么我们被迫使用指向成员的指针,而不仅仅是一个清晰的 switch 语句,甚至是 else-ifs?
谢谢。
你对成员函数指针数组与switch指令优缺点的分析已经很不错了。
但这完全取决于上下文:
当然从技术上讲,你是完全正确的:如果你只是想更换一个开关,这样的阵列是非常麻烦的。更不用说可以使用 jump tables 优化开关的编译器,它使用的间接寻址比您的数组少一个。
但是您的示例代码实现了一种command design pattern。从设计的角度来看,这在进化性和可维护性方面可能具有重要的优势,这些优势超过了技术缺陷。例如,它可以很容易地用在应用程序中以实现 undo/redo 功能。它还简化了多个同时用户界面允许在对象上触发这些命令的情况(例如:命令行 window 和 GUI)
视情况而定。具有多个 未连接 选择的 Switch-case 实际上与大 if-else
相同 - 慢。好的优化是使用偏移table(或跳转table)执行所需的操作,建议您实现。
奇怪的是,编译器通常可以自动执行这种优化 - 如果 switch-case
写得好。
但是写得好是什么意思?
这意味着,你必须设计条目索引,这样才能轻松快速地计算出需要执行的条目的位置。考虑以下代码:
int n = 0;
std::cin >> n;
if(n == 1) printf("1\n");
else if(n == 2) printf("2\n");
else if(n == 3) printf("3\n");
else if(n == 4) printf("4\n");
这是可能的输出(实际输出,在 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("1\n"); break;
case 2: printf("2\n"); break;
case 3: printf("3\n"); break;
case 4: printf("4\n"); 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
我没有 post 调用 printf
- 基本相同,但没有任何 cmp
或跳转指令。
这个输出当然只是许多可能的输出之一,但重点是:设计良好的应用程序在条件部分进行智能优化,可以执行得更高效。在这里,编译器能够直接跳转到正确的指令,因为可以很容易地计算出它的偏移量——所有情况都用数字标记,数字增加一。
为了更直接地回答你的问题:你得到的建议在技术上是正确的,但不是复杂的代码(可能会或可能不会显着提高速度),我会比每个人都更关注编译器友好的优化可以理解和依赖(只要编译器足够聪明,可以利用这一优势并生成优化代码)。
上下文很重要。
如果您使用 PC,我认为更喜欢阵列,因为与非常比较相比,获得结果的速度非常快,购买您的内存。 这在内存中很昂贵,但对于非常大的数组来说速度很快。
如果context是单片机,内存很贵,不能浪费去存所有数组。特别是如果阵列几乎不被使用。 但是可以优先选择开关,因为不使用内存,而且微控制器中的汇编程序非常快。
- 如果你有那么多内存和一门高级编程语言,也许数组更好。
- 如果你的内存很少,并且使用汇编程序或微型 C 等低级编程语言,最好是开关(或 rom 表)