使编译器假设所有情况都在开关中处理,而不使用默认值

Make compiler assume that all cases are handled in switch without default

本文关键字:处理 默认值 假设 编译器 情况 开关      更新时间:2023-10-16

让我们从一些代码开始。这是我的程序的一个极其简化的版本。

#include <stdint.h>
volatile uint16_t dummyColorRecepient;
void updateColor(const uint8_t iteration)
{
uint16_t colorData;
switch(iteration)
{
case 0:
colorData = 123;
break;
case 1:
colorData = 234;
break;
case 2:
colorData = 345;
break;
}
dummyColorRecepient = colorData;
}
// dummy main function
int main()
{
uint8_t iteration = 0;
while (true)
{
updateColor(iteration);
if (++iteration == 3)
iteration = 0;
}
}

程序编译时出现警告:

./test.cpp: In function ‘void updateColor(uint8_t)’:
./test.cpp:20:25: warning: ‘colorData’ may be used uninitialized in this function [-Wmaybe-uninitialized]
dummyColorRecepient = colorData;
~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~

正如您所看到的,变量iteration始终是012是绝对确定的。然而,编译器并不知道这一点,它假设开关可能不会初始化colorData。(编译过程中的任何数量的静态分析在这里都没有帮助,因为真正的程序分布在多个文件上。)

当然,我可以添加一个默认语句,比如default: colorData = 0;,但这会为程序添加额外的24个字节这是一个用于微控制器的程序,我对它的大小有非常严格的限制

我想通知编译器,这个开关保证覆盖iteration的所有可能值。

正如您所看到的,变量迭代总是0、1或2是绝对确定的。

从工具链的角度来看,这不是真的。您可以从其他地方调用此函数,甚至可以从其他翻译单元调用。唯一强制执行约束的地方是main,即使在那里,编译器也很难推理

不过,出于我们的目的,让我们把你不会链接任何其他翻译单元视为阅读,我们想告诉工具链这一点。幸运的是,我们可以!

如果你不介意不可启动,那么GCC的__builtin_unreachable内置通知它,default的情况预计不会达到,应该被认为是不可访问的。我的GCC足够聪明,知道这意味着colorData永远不会被忽视,除非所有的赌注都被取消。

#include <stdint.h>
volatile uint16_t dummyColorRecepient;
void updateColor(const uint8_t iteration)
{
uint16_t colorData;
switch(iteration)
{
case 0:
colorData = 123;
break;
case 1:
colorData = 234;
break;
case 2:
colorData = 345;
break;
// Comment out this default case to get the warnings back!
default:
__builtin_unreachable();
}
dummyColorRecepient = colorData;
}
// dummy main function
int main()
{
uint8_t iteration = 0;
while (true)
{
updateColor(iteration);
if (++iteration == 3)
iteration = 0;
}
}

(现场演示)

这不会添加实际的default分支,因为其中没有"代码"。事实上,当我使用带有-O2的x86_64 GCC将其插入Godbolt时,添加此分支后的程序比没有添加的程序小—从逻辑上讲,您刚刚添加了优化提示。

实际上,有人建议将其作为C++中的标准属性,这样它在未来可能会成为一个更具吸引力的解决方案。

使用"立即调用的lambda表达式"习语和assert:

void updateColor(const uint8_t iteration)
{
const auto colorData = [&]() -> uint16_t
{
switch(iteration)
{
case 0: return 123;
case 1: return 234;
}
assert(iteration == 2);
return 345;
}();
dummyColorRecepient = colorData;
}
  • lambda表达式允许您将colorData标记为constconst变量必须始终初始化。

  • assert+return语句的组合允许您避免警告并处理所有可能的情况。

  • assert不会在发布模式下编译,从而避免了开销。


您也可以考虑函数:

uint16_t getColorData(const uint8_t iteration)
{
switch(iteration)
{
case 0: return 123;
case 1: return 234;
}
assert(iteration == 2);
return 345;
}
void updateColor(const uint8_t iteration)
{
const uint16_t colorData = getColorData(iteration);
dummyColorRecepient = colorData;
}

您只需在其中一种情况中添加default标签,就可以在没有警告的情况下编译它:

switch(iteration)
{
case 0:
colorData = 123;
break;
case 1:
colorData = 234;
break;
case 2: default:
colorData = 345;
break;
}

或者:

uint16_t colorData = 345;
switch(iteration)
{
case 0:
colorData = 123;
break;
case 1:
colorData = 234;
break;
}

两者都试试,用两者中较短的一个。

我知道有一些很好的解决方案,但也可以在编译时知道If的值,而不是开关语句,您可以将constexprstatic function template和几个enumerators一起使用;它在一个类中看起来像这样:

#include <iostream>
class ColorInfo {
public:
enum ColorRecipient {
CR_0 = 0,
CR_1,
CR_2
};
enum ColorType {
CT_0 = 123,
CT_1 = 234,
CT_2 = 345
};
template<const uint8_t Iter>
static constexpr uint16_t updateColor() {
if constexpr (Iter == CR_0) {
std::cout << "ColorData updated to: " << CT_0 << 'n';
return CT_0;
}
if constexpr (Iter == CR_1) {
std::cout << "ColorData updated to: " << CT_1 << 'n';
return CT_1;
}
if constexpr (Iter == CR_2) {
std::cout << "ColorData updated to: " << CT_2 << 'n';
return CT_2;
}
}
};
int main() {
const uint16_t colorRecipient0 = ColorInfo::updateColor<ColorInfo::CR_0>();
const uint16_t colorRecipient1 = ColorInfo::updateColor<ColorInfo::CR_1>();
const uint16_t colorRecipient2 = ColorInfo::updateColor<ColorInfo::CR_2>();
std::cout << "n--------------------------------n";
std::cout << "Recipient0: " << colorRecipient0 << 'n'
<< "Recipient1: " << colorRecipient1 << 'n'
<< "Recipient2: " << colorRecipient2 << 'n';
return 0;
}

if constexpr中的cout语句只是为了测试目的而添加的,但这应该说明另一种不必使用switch语句的可能方法,前提是您的值在编译时是已知的。如果这些值是在运行时生成的,我不完全确定是否有办法使用constexpr来实现这种类型的代码结构,但如果有,如果其他有经验的人能够详细说明如何使用runtime值使用constexpr来实现这一点,我将不胜感激。然而,由于没有magic numbers,因此此代码可读性很强,并且代码非常有表现力。


-更新-

在阅读了更多关于constexpr的内容后,我注意到它们可以用于生成compile time constants。我还了解到,它们不能生成runtime constants,但可以在runtime function中使用。我们可以采用上面的类结构,并在运行时函数中使用它,方法是将static function添加到类中:

static uint16_t colorUpdater(const uint8_t input) {
// Don't forget to offset input due to std::cin with ASCII value.
if ( (input - '0') == CR_0)
return updateColor<CR_0>();
if ( (input - '0') == CR_1)
return updateColor<CR_1>();
if ( (input - '0') == CR_2)
return updateColor<CR_2>();
return updateColor<CR_2>(); // Return the default type
}

但是,我想更改这两个函数的命名约定。我将把第一个函数命名为colorUpdater(),而我刚刚展示的这个新函数,我将把它命名为updateColor(),因为这样看起来更直观。所以更新后的类现在看起来是这样的:

class ColorInfo {
public:
enum ColorRecipient {
CR_0 = 0,
CR_1,
CR_2
};
enum ColorType {
CT_0 = 123,
CT_1 = 234,
CT_2 = 345
};
static uint16_t updateColor(uint8_t input) {
if ( (input - '0') == CR_0 ) {
return colorUpdater<CR_0>();
}
if ( (input - '0') == CR_1 ) {
return colorUpdater<CR_1>();
}
if ( (input - '0') == CR_2 ) {
return colorUpdater<CR_2>();
}
return colorUpdater<CR_0>(); // Return the default type
}
template<const uint8_t Iter>
static constexpr uint16_t colorUpdater() {
if constexpr (Iter == CR_0) {
std::cout << "ColorData updated to: " << CT_0 << 'n';
return CT_0;
}
if constexpr (Iter == CR_1) {
std::cout << "ColorData updated to: " << CT_1 << 'n';
return CT_1;
}
if constexpr (Iter == CR_2) {
std::cout << "ColorData updated to: " << CT_2 << 'n';
return CT_2;
}
}
};

如果只想将其用于编译时常数,则可以像以前一样使用它,但要使用函数的更新名称。

#include <iostream>
int main() {
auto output0 = ColorInfo::colorUpdater<ColorInfo::CR_0>();
auto output1 = ColorInfo::colorUpdater<ColorInfo::CR_1>();
auto output2 = ColorInfo::colorUpdater<ColorInfo::CR_2>();
std::cout << "n--------------------------------n";
std::cout << "Recipient0: " << output0 << 'n'
<< "Recipient1: " << output1 << 'n'
<< "Recipient2: " << output2 << 'n';
return 0;
}

如果您想将这种机制与runtime值一起使用,您可以简单地执行以下操作:

int main() {
uint8_t input;
std::cout << "Please enter input value [0,2]n";
std::cin >> input;
auto output = ColorInfo::updateColor(input);
std::cout << "Output: " << output << 'n';
return 0;
}

这将适用于运行时值。

好吧,如果你确定你不必处理其他可能的值,你可以使用算术。摆脱了他的分支和负载。

void updateColor(const uint8_t iteration)
{
dummyColorRecepient = 123 + 111 * iteration;
}

我将在Orbit的答案中扩展Lightness Races。

我目前使用的代码是:

#ifdef __GNUC__
__builtin_unreachable();
#else
__assume(false);
#endif

CCD_ 40适用于GCC和Clang,但不适用于MSVC。我使用__GNUC__来检查它是否是前两个编译器中的一个(或另一个兼容的编译器),并将__assume(false)用于MSVC。