【问题标题】:Elegant way of avoiding default in switch cases (using enum class)在开关情况下避免默认的优雅方式(使用枚举类)
【发布时间】:2021-09-22 08:18:14
【问题描述】:

我有一个枚举类,例如:

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)的可能性。

【问题讨论】:

  • break of S3 是不需要的。
  • case 子句 "fall-through" 这样您就可以将所有未使用的案例与单个 break 组合在一起(以及向您未来的自己解释这一点的评论)。
  • 顺便说一句,对于值和(未参数化的)动作之间的简单 1:1 关系,如这里,您可以使用由枚举值索引的函子/函数指针数组,或者如果枚举是映射不连续且基于 0。诚然,我看不出这将如何解决您的完整性问题。
  • 在某些特殊情况下可以使用 if 语句代替 switch。
  • 很遗憾编译器是对的,因为您没有涵盖所有情况;不幸的是,default 是必需的,并且不应为空(相反,它应该引发断言失败)。原因是您的枚举变量可以包含与您声明的值不同的值。

标签: c++ enums switch-statement c++17


【解决方案1】:

对于 switch 案例,我更喜欢这样做:

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,并在适当的时候放弃测试用例。

【讨论】:

  • 也许你可以用默认情况抛出异常,这样用户就可以记住他们没有添加新的情况。
  • throw 在 switch 案例之外,因为枚举的值可能超出其提供的值,因此您应该对“并非所有路径都返回非 void 返回类型函数”发出警告。
  • @enumerator: 不在default 中,正如 OP 所提到的,但外面没问题。
【解决方案2】:

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

这个想法是生成枚举本身以及开关情况和容器,例如地图或数组来自相同的文本,它们必须在一个单独的文件中,以便可以包含相同的副本几次。这使得在没有相关操作的情况下拥有枚举成员在物理上是不可能的。添加另一个状态就像在 enum-actions.h 中添加一行一样简单;从该列表生成的所有用途,如地图条目、开关案例或 else-if 链都会自动适应。这些地方因为include依赖还是需要重新编译的,但是不需要动。

这是包含枚举成员名称、值和相关操作的文件(在生成 switch/case 时必须是函数名称或仿函数)。我给了它一个 .h 后缀,因为它将被包含在内,即使它不是语法 C++;也可以给它一个 .txt 后缀。

enum-actions.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)

stateE.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
};

associative-enum.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

【讨论】:

  • 你的想法好像和 LLVM 的代码库中使用的一样,很酷
  • @prehistoricpenguin 啊,我不知道!我什至不知道 20 年前我是从谁那里学来的,也不知道是不是我自己“发明”的。在任何情况下,这都是一种绝对不会忽视任何价值的方法。
【解决方案3】:

我们也可以使用关联容器来存储handlers(lambdas或者functors),然后在找到key之后调用handlers。

添加use assertion以确保所有枚举都有对应的处理程序(但这不是编译时错误,我们可以在此处存储函数指针以获得编译时检查,但这会使代码变得丑陋)。

#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;
}

【讨论】:

  • 我喜欢容器,但使用映射不可能有编译时断言以确保完整性(OP 想要)。使用数组而不是映射将允许这样做,但仅限于连续的、基于 0 的枚举。人们应该认为一些聪明的模板元编程应该是可能的;或者可能是带有用户定义文字的用户定义类型?
【解决方案4】:

如果您使用编译器 g++,则可以添加编译器选项 -Wall 或 -Wswitch。例如,g++ -std=c++17 -Wall main.cppg++ -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]

【讨论】:

  • 如果此处不需要处理 S2,则需要忽略警告,但会掩盖添加了确实需要处理的 S3 的情况。 OP 希望在没有警告和 default 的情况下进行编译,但 also 没有针对未处理值的显式空案例。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-09-16
  • 1970-01-01
  • 2019-06-16
  • 2013-04-09
  • 2012-11-04
  • 1970-01-01
相关资源
最近更新 更多