开关的默认标签应该放什么?

11

假设我有一个枚举。

enum class ShapeName : char {TRIANGLE,CIRCLE,SQUARE};

之后我有一个像这样的函数:

void Function (ShapeName const shape){

    switch (shape){
        case ShapeName::TRIANGLE:
            DoSomething1();
            break;

        case ShapeName::CIRCLE:
            DoSomething2();
            break;

        case ShapeName::SQUARE: 
            DoSomething3();
            break;

        default:
            //THIS CODE BLOCK SHOULD NEVER BE EXECUTED!
    }

    return;
}
尽管默认标签不应该执行,但我想考虑到程序员在“ShapeName”中添加另一个值时可能出现的潜在错误。
你会建议我做什么? 1. 断言
我可以使用断言,但我要断言什么呢?
assert(false); //?

2. 异常
我可以抛出一个异常,但我认为这不是很好的实践。 我的印象是,异常是用于不能预测的运行时事件的,因为某些环境原因。

3. 退出
我可以立即以错误退出程序。感觉这是最好的想法,但我不确定这是否是良好的实践。 我认为断言的优点在于,当你准备好发布程序时,可以关闭所有断言,然后所有的断言代码都将不存在。


也许还有其他我不知道的方法。我确实使用了一个编译器标志来警告未考虑的值,但我仍然想知道其他人的建议。


  1. 你能告诉编译器为此发出警告吗?对于旧式枚举,我期望有这样的选项。
- Steve Jessop
@SteveJessop g++ -w -Wall -Wextra -Wswitch -Wswitch-default -Wswitch-enum -std=c++0x -o main main.cpp 奇怪的是,我用上面的代码没有收到任何警告。我添加了另一个ShapeName并去掉了默认标签。仍然没有任何提示! - Trevor Hickey
糟糕,C++11的实现还没有完成,不过我们可以抱有希望。 - Steve Jessop
可能是Does "default" switch case disturb jump table optimization?的重复问题。 - Johannes Schaub - litb
4个回答

9
我喜欢使用信息性消息来进行断言。尝试这样做:
assert (!"The default case of so-so switch was reached.");

这个语句总是返回false,但是提供了一条你可以使用的消息。

编辑:
我找到了从记忆中想起这个概念的来源;它在以下书籍中:
C++编码规范-101条规则和指南


2
或者如果你不喜欢这种拼写方式,可以使用assert(false && "message"); - Steve Jessop
3
记住,断言通常会从“发布版”中删除。 - user405725
1
对于发布版本,您还可以使用编译器等效的 __assume(0) 作为优化提示。 - ildjarn
感觉有点像巧妙的黑科技。同时也感觉是最好的解决方案。感谢来源 - Trevor Hickey
1
我喜欢写像assert(not "reachable");这样的代码。 - porglezomp

4

在这种特定情况下,当您切换枚举标签并处理所有情况时,我建议省略默认项。这样一来,对于任何合理的编译器,如果有人向枚举添加新标签而未添加到switch语句中,您将收到关于该标签未在switch语句中处理的编译时警告。


显然,GCC在处理新的C++11枚举类方面有些不合理,尽管提问者没有说明使用的是哪个版本的GCC。 - Steve Jessop
@gcc (Ubuntu/Linaro 4.6.1-9ubuntu3) 4.6.1 - Trevor Hickey
@SteveJessop:如果我删除默认并添加“OTHER”标签,gcc(尝试了4.4.5、4.5.2和4.6.3)会给我“警告:在开关中未处理枚举值‘OTHER’”。 - Chris Dodd

0

简短回答:使用多态性来消除问题。

长话短说:解决根本问题(即如何防止遗漏条目的开关语句?)最简单的方法是避免在可能忘记条目的情况下使用开关语句。在局部上下文中,开关语句很有用,但如果它们所代表的逻辑在多个位置中被复制,则有可能会忘记更新其中一个,从而为运行时错误设置自己。

class Shape
{
    // functionality MUST be added for new shape types
    // or a compile error will occur
    virtual void DoSomething() const = 0;
};

void Function (Shape const & shape){
    return shape.DoSomething();
}

在创建现场,switch语句仍然可能是有用的:

enum class ShapeName : char {TRIANGLE,CIRCLE,SQUARE};

unique_ptr< Shape > MakeShape (ShapeName const shape){
    switch (shape){
        case ShapeName::TRIANGLE:
            return unique_ptr< Shape >( new Triangle() );

        case ShapeName::CIRCLE:
            return unique_ptr< Shape >( new Circle() );

        case ShapeName::SQUARE: 
            return unique_ptr< Shape >( new Square() );
     }

     throw std::runtime_error();
     // or whichever option you prefer from the other answers
}

但这是逻辑存在的唯一地方。甚至可以通过静态多态性来改进:

enum class ShapeName : char {TRIANGLE,CIRCLE,SQUARE,ELLIPSE};

template< ShapeName shape >
unique_ptr< Shape > MakeShape();

template<>
unique_ptr< Shape > MakeShape< ShapeName::TRIANGLE >()
{
    return unique_ptr< Shape >( new Triangle() );
}

template<>
unique_ptr< Shape > MakeShape< ShapeName::CIRCLE >()
{
    return unique_ptr< Shape >( new Circle() );
}

template<>
unique_ptr< Shape > MakeShape< ShapeName::SQUARE >()
{
    return unique_ptr< Shape >( new Square() );
}

int main()
{
    MakeShape< ShapeName::TRIANGLE >(); // okay

    MakeShape< ShapeName::ELLIPSE >(); // compile error
}

查看Martin Folwer以获取更多信息。


为什么要踩一下?我认为对于这个人为的例子,这是最好的解决方案。switch语句可能会成为代码异味,正是因为这个原因。default的问题在于,如果省略了一个case,你不会得到编译器警告;如果没有default,如果你处于每个case都返回的switch中(例如工厂),编译器将抱怨函数末尾缺少返回。我认为MSVC曾经有一个无法到达的宏来帮助这里。 - Andrew Lazarus

0

我想对我来说,这在某种程度上取决于DoSomething#方法的作用。

如果它会导致稍后发生不太优雅的崩溃,那么当调用此函数时,您肯定希望中断运行时,并解释“使用无效枚举调用函数:enumName”之类的内容,以便开发人员得到通知,他们没有更新此switch语句,而不是让他们想知道是否稍后会出现故障。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接