在我的代码中,我习惯于编写包含类似下面的断言的后备默认情况,以防止在语义改变的情况下忘记更新开关
switch(mode) {
case ModeA: ... ;
case ModeB: ... ;
case .. /* many of them ... */
default: {
assert(0 && "Unknown mode!");
return ADummyValue();
}
};
现在我想知道人工回退检查默认情况是否会干扰跳表生成?想象一下,“ModeA”和“ModeB”等是连续的,所以编译器可以优化成一个表。由于“默认”情况包含一个实际的“返回”语句(因为断言将在发布模式中消失,并且编译器会抱怨缺少返回语句),编译器似乎不太可能优化默认分支。
处理这个问题的最佳方法是什么?有些朋友建议我用空指针取消引用替换“ADummyValue”,以便编译器在存在未定义的行为时可以省略警告丢失的return语句。有没有更好的方法来解决这个问题?
至少在我看过的编译器中,答案通常是否定的。他们中的大多数会像这样编译一个switch语句,代码大致相当于:
if (mode < modeA || mode > modeLast) {
assert(0 && "Unknown mode!");
return ADummyValue();
}
switch(mode) {
case modeA: ...;
case modeB: ...;
case modeC: ...;
// ...
case modeLast: ...;
}
如果你使用“默认”(哈!) <assert.h>
,无论如何,定义都与NDEBUG宏相关联,所以也许只是
case nevermind:
#if !defined(NDEBUG)
default:
assert("can" && !"happen");
#endif
}
我只看到一个解决方案,以防优化实际上受到干扰:臭名昭着的“#ifndef NDEBUG”绕过默认情况。这不是最好的技巧,但在这种情况下很明显。
顺便说一句:您是否已经了解了编译器的默认情况?
如果你有一个永远不应该达到的状态,那么你应该杀死程序,因为它刚刚达到意想不到的状态,即使在发布模式下(你可能只是更外交,实际上保存用户数据并做其他所有好东西在下降之前)。
除非您实际测量(使用分析器)您需要它们,否则请不要过度使用微优化。
处理此问题的最佳方法是不禁用断言。这样你也可以留意可能的错误。有时候,应用程序崩溃时会有一个好消息来解释究竟发生了什么,然后继续工作。
使用编译器扩展:
// assume.hpp
#pragma once
#if defined _MSC_VER
#define MY_ASSUME(e) (__assume(e), (e) ? void() : void())
#elif defined __GNUC__
#define MY_ASSUME(e) ((e) ? void() : __builtin_unreachable())
#else // defined __GNUC__
#error unknown compiler
#endif // defined __GNUC__
-
// assert.hpp
#include <cassert>
#include "assume.hpp"
#undef MY_ASSERT
#ifdef NDEBUG
#define MY_ASSERT MY_ASSUME
#else // NDEBUG
#define MY_ASSERT assert
#endif // NDEBUG