避免 switch 情况下默认的优雅方法(使用枚举类)

问题描述 投票:0回答:5

我有一个枚举类,例如:

enum class State{
            S1,
            S2,
            S3,
            S4
        };

每当我制作可能使用此类的 switch/case 语句时,我都想不惜一切代价避免使用“默认”,以迫使我为所有可能的情况编写语句。这个想法是,如果我在这个枚举中添加一个新的情况“S5”,由于缺乏默认值,编译器将在每次切换时向我发送警告,因为并非所有情况都被覆盖。通过这样做,我最终不会忘记这个新状态可能需要实现特定行为的地方。

问题是,有各种开关/情况,其中仅实现了某些枚举情况:

switch(state)
{
 case S1: 
          doSomething1();
          break;
 case S2: 
          doSomething2();
          break;
 case S3: 
          break;
 case S4:
          break;
}

但是我不太喜欢这些没有行为的状态的各种“空”情况,然后休息。这正是“默认”更优雅的地方,但正如我刚才所解释的,这就是我想要避免的。

有没有更优雅(或更有效?)的方法来实现我在这里想做的事情?因为我知道其他编程语言为 switch/case 语句提供了更高级的语法,但我不确定 C++(更具体地说是 C++17)的可能性。

c++ enums switch-statement c++17
5个回答
2
投票

对于开关盒,我更喜欢这样做:

enum class my_enum {
    E1,
    E2,
    E3,
    E4,
    E5
};

result_type do_action_based_on_enum(my_enum e)
{
    switch(e) {
    case my_enum::E1:
        return action_e1();
    case my_enum::E2:
        return action_e2();
    case my_enum::E3:
    case my_enum::E4:
    case my_enum::E5:
        return action_default();
    }
}

返回每种情况,以避免每次都写

break
,并在适当的时候放弃测试用例。


1
投票

“优雅”是情人眼里出西施;但我发现使用宏生成处理枚举的代码序列大大减少了冗余和出错的机会。它是使用预处理器的少数合理情况之一(除了条件编译和包含之外)。通常情况下,这会在大型分布式项目中带来更多红利。

这个想法是从相同的文本生成枚举本身以及开关案例和容器,例如映射或数组,必须位于单独的文件中,以便可以多次包含相同的副本。这使得在没有相关操作的情况下“物理上不可能”拥有枚举成员。添加另一个状态就像在 enum-actions.h 中添加一行一样简单;所有使用(如映射条目、switch case 或从该列表生成的 else-if 链)都会自动适应。这些地方仍然需要重新编译,因为包含依赖,但不需要触及。 这是包含枚举成员名称、值和关联操作的文件(在生成 switch/case 时必须是函数名称或函子)。我给它一个 .h 后缀,因为它将被包含在内,即使它不是语法上的 C++;也可以给它一个 .txt 后缀。

枚举动作.h

// A list of enum identifiers with associated values // and actions usable in macro definitions. // This macro is used to construct both the actual enum // as well as the cases in the action switch. This way // it is impossible to have enum members without an associated action. // There is no "real" enum definition elsewhere; this is it. ENUM_ACTION(S1, 2, action1) ENUM_ACTION(S2, 23, action2) ENUM_ACTION(S3, 997, no_action)

状态E.h

// Define the states enum from the macro list of enum/action pairs. // The last member will have a trailing comma as well, which // is permitted by the C++ grammar exactly for this use case of code generation. enum stateE { # define ENUM_ACTION(state, value, action) state = value, # include "enum-actions.h" # undef ENUM_ACTION };

关联枚举.cpp

#include <iostream> #include "stateE.h" // Dummy actions for states void action1() { std::cout << __func__ << "\n"; } void action2() { std::cout << __func__ << "\n"; } // pseudo action when nothing should happen void no_action() { std::cout << __func__ << "\n"; } /// Perform the action associated with the state. This is done with a /// switch whose cases are constructed from the list /// in enum-actions.h. void actOnState(stateE stateArg) { switch (stateArg) { # define ENUM_ACTION(state, value, action) case state: action(); break; # include "enum-actions.h" # undef ENUM_ACTION } } int main() { actOnState(S1); actOnState(S2); actOnState(S3); }

示例会话:

$ g++ -Wall -o associative-enum associative-enum.cpp && ./associative-enum action1 action2 no_action

我们还可以使用关联容器来存储处理程序(lambda 或函子),然后在找到密钥后调用处理程序。

1
投票
添加使用断言以确保所有枚举都有相应的处理程序(但这不是编译时错误,我们可以在此处存储函数指针以进行编译时检查,但这会使代码变得丑陋)。

#include <cassert> #include <functional> #include <iostream> #include <map> enum class State { S1, S2, S3, S4, Count }; int main(int argc, char* argv[]) { auto dummy_handler = []() {}; std::map<State, std::function<void()>> mapping = { {State::S1, []() { std::cout << "s1\n"; }}, {State::S2, []() { std::cout << "s2\n"; }}, {State::S3, dummy_handler }, {State::S4, dummy_handler }, }; assert(mapping.size() == static_cast<size_t>(State::Count)); //May use this // line to ensume all handlers are set auto dispatch = [&](State e) { if (auto itr = mapping.find(e); itr != mapping.end()) { itr->second(); } }; auto e = State::S1; // Handlers for s1 will be called dispatch(e); e = State::S3; dispatch(e); // handler for s3 return 0; }

如果您使用编译器 g++,则可以添加编译器选项 -Wall 或 -Wswitch。例如,

0
投票

g++ -std=c++17 -Wswitch main.cpp

。求代码
#include <iostream>

enum class State{
            S1,
            S2
        };

int main()
{
    State state = State::S1;
    switch (state) {
        case State::S1:
            std::cout << "State::S1" << std::endl;
            break;
    }
}

你可能会得到编译错误:
main.cpp:11:12: warning: enumeration value 'S2' not handled in switch [-Wswitch]

相当旧的线程,但我想我可以添加一个我发现的解决方案,但这里没有建议。如果 switch 语句没有使用所有枚举值,我正在寻找一种完全阻止编译的方法,所以我最终做的是以下内容:

0
投票
enum class MyEnum : uint8 { E1, E2, Count } void RandomFunc() { switch (MyEnum) { case MyEnum::E1: /* Do your things */ break; case MyEnum::E2: /* Do your things */ break; default: static_assert(static_cast<uint8>(MyEnum::Count) == 2, "If you removed or added a MyEnum value, please update the code above"); } }

在大多数情况下,static_assert 本身需要位于默认语句中,以避免收到警告(我不喜欢我的解决方案的这一部分,但这是迄今为止我发现的最好的解决方案)。这确保任何人都无法在不更新关联代码的情况下添加或删除枚举值。 但是,在您的具体情况下,我认为将其放在 switch 语句之外并为您的默认行为使用默认情况是安全的。
enum class MyEnum : uint8
{
    E1,
    E2,
    E3,
    E4,
    Count
}

void RandomFunc()
{
    static_assert(static_cast<uint8>(MyEnum::Count) == 4, "If you removed or added a MyEnum value, please update the switch statement below");
    switch (MyEnum)
    {
        case MyEnum::E1: // Do your things
        case MyEnum::E2: // Do your things
        default: // Your default behaviour for E3 and E4
    }
}

如果您确实不想使用默认语句,尽管使用了单独的 static_assert,则可以使用其他答案中描述的失败方法。
非编译时错误生成也可能是添加一个

check(false)

默认情况下的

语句(或您的项目使用的任何错误语句)。这使得任何阅读您的代码的人都清楚地知道不需要默认语句,并且他们需要更新案例。 我不建议同时执行这两项操作,因为 static_assert 已经涵盖了编译时,因此覆盖运行时没有多大意义。但它也提高了可读性,所以我可以理解使用两者。
    

© www.soinside.com 2019 - 2024. All rights reserved.