使编译器假设所有情况都在开关中处理,而不使用默认值
Make compiler assume that all cases are handled in switch without default
让我们从一些代码开始。这是我的程序的一个极其简化的版本。
#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
始终是0
、1
或2
是绝对确定的。然而,编译器并不知道这一点,它假设开关可能不会初始化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
标记为const
。const
变量必须始终初始化。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
的值,而不是开关语句,您可以将constexpr
与static 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。
- 如何创建一个CMake变量,除非显式重写,否则使用默认值
- 具有默认值的引用获取函数
- 当给定默认值时,为什么此模板参数推导失败
- 从具有默认值的部分指定模板类继承时发生SWIG错误,具有不带默认值的正向声明
- 格式化浮点值:返回默认值
- 如何将数组部分初始化为某个默认值?
- asn1c 不会从 asn.1 模块中提取八位字节字符串的默认值
- 创建一个包含 c++ 默认值的环境文件
- C++(和 ROS) - 包含与前向声明引用,设置默认值和类型定义
- Makefile g++ 使用命令行中的 -D 变量进行编译,默认值
- Switch 语句(字符串)一直选择默认值,除非其为零
- 如何使用默认值将枚举声明为 extern
- 如何在提升程序选项中设置矢量<矢量>的默认值<string>
- 将错误处理添加到加载数据的函数(但有时还需要使用默认值)
- 使编译器假设所有情况都在开关中处理,而不使用默认值
- 检查信号处理程序是否不是默认值
- C++C-tor处理默认成员值构造异常
- 将包装器与函数指针一起使用来处理参数的默认值
- 使用boost program_options处理帮助消息,删除默认值或重新格式化帮助消息
- 如何处理(深度)嵌套函数调用中的默认值