在将标题分为8个方向时,如何避免if/else if chain?

Ora*_*kia 111 c++ if-statement

我有以下代码:

if (this->_car.getAbsoluteAngle() <= 30 || this->_car.getAbsoluteAngle() >= 330)
  this->_car.edir = Car::EDirection::RIGHT;
else if (this->_car.getAbsoluteAngle() > 30 && this->_car.getAbsoluteAngle() <= 60)
  this->_car.edir = Car::EDirection::UP_RIGHT;
else if (this->_car.getAbsoluteAngle() > 60 && this->_car.getAbsoluteAngle() <= 120)
  this->_car.edir = Car::EDirection::UP;
else if (this->_car.getAbsoluteAngle() > 120 && this->_car.getAbsoluteAngle() <= 150)
  this->_car.edir = Car::EDirection::UP_LEFT;
else if (this->_car.getAbsoluteAngle() > 150 && this->_car.getAbsoluteAngle() <= 210)
  this->_car.edir = Car::EDirection::LEFT;
else if (this->_car.getAbsoluteAngle() > 210 && this->_car.getAbsoluteAngle() <= 240)
  this->_car.edir = Car::EDirection::DOWN_LEFT;
else if (this->_car.getAbsoluteAngle() > 240 && this->_car.getAbsoluteAngle() <= 300)
  this->_car.edir = Car::EDirection::DOWN;
else if (this->_car.getAbsoluteAngle() > 300 && this->_car.getAbsoluteAngle() <= 330)
  this->_car.edir = Car::EDirection::DOWN_RIGHT;
Run Code Online (Sandbox Code Playgroud)

我想避开ifs链; 真的很难看.是否有另一种可能更清晰的写作方式?

Bor*_*der 176

#include <iostream>

enum Direction { UP, UP_RIGHT, RIGHT, DOWN_RIGHT, DOWN, DOWN_LEFT, LEFT, UP_LEFT };

Direction GetDirectionForAngle(int angle)
{
    const Direction slices[] = { RIGHT, UP_RIGHT, UP, UP, UP_LEFT, LEFT, LEFT, DOWN_LEFT, DOWN, DOWN, DOWN_RIGHT, RIGHT };
    return slices[(((angle % 360) + 360) % 360) / 30];
}

int main()
{
    // This is just a test case that covers all the possible directions
    for (int i = 15; i < 360; i += 30)
        std::cout << GetDirectionForAngle(i) << ' ';

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

我就是这样做的.(根据我之前的评论).

  • 我字面上认为我在那里看到了Konami Code代替枚举. (92认同)
  • 这是非常聪明的,但它完全不可读,并且不太可能是可维护的,所以我不同意它是对原始的"丑陋"的一个很好的解决方案.我想,这里有个人品味的元素,但我发现x4u和motoDrizzt的清理分支版本更受欢迎. (33认同)
  • @CodesInChaos:C99和C++与C#具有相同的要求:如果`q = a/b`和`r = a%b`则那么`q*b + r`必须等于'a`.因此,在C99中,剩余部分为负数是合法的.BorgLeader,你可以用`(((angle%360)+ 360)%360)/ 30`解决问题. (21认同)
  • @ericlippert,你和你的计算数学知识继续留下深刻的印象. (7认同)
  • @cyanbeam在main中的for循环只是一个"演示",`GetDirectionForAngle`是我提议的替代if/else级联,它们都是O(1)...... (4认同)
  • 值得注意的是,当角度为"30"时,代码会获得不同的结果. (2认同)
  • @IMSoP我不同意可维护性.假设您现在只处理左/右/上/下.现在更改它,以便您处理8个方向,就像当前的答案一样.哪个解决方案需要对代码进行更多更改,并且更有可能引入错误?我说问题中的原始方法. (2认同)
  • @milleniumbug这取决于*它如何变化.如果你想让一些扇区宽22.5度(圆圈的十六分之一)怎么办?或者一些非常小的扇区,所以查找表变成了20个项目,大部分重复了?也许这是关于你如何看待问题:你看看它们之间的界限,还是它们之间的界限?我发现"在30度绘制一个边界,然后在60度绘制一个边界,然后在120度绘制一个边界......"比"划分为30度线段,将线段1标记为右边,第2段标记为UP_RIGHT,第3段为UP,第4段也作为UP ...". (2认同)
  • 我必须接受"聪明"批评.虽然原作如果绝对是丑陋的,但它至少对未来的意图是明确的,你将不得不阅读它. (2认同)
  • @Borgleader:实际上,if/else级联是O(n)而不是O(1),其中n是子句的数量.更重要的是,对于小n,这是无分支的,并编译为一些快速指令.查找表可能会在缓存中丢失,但如果重复使用将保持热.(即,如果此函数是性能关键循环的一部分,那么即使每次都使用不同的角度它也会表现良好,与if/else链不同.如果很少使用此函数,它可能会很慢,但它不会对整体性能有很大的影响.如果`enum`是32b,这将有助于确保表条目为8位...) (2认同)

Ste*_*mer 71

您可以map::lower_bound在地图中使用和存储每个角度的上限.

下面的工作示例:

#include <cassert>
#include <map>

enum Direction
{
    RIGHT,
    UP_RIGHT,
    UP,
    UP_LEFT,
    LEFT,
    DOWN_LEFT,
    DOWN,
    DOWN_RIGHT
};

using AngleDirMap = std::map<int, Direction>;

AngleDirMap map = {
    { 30, RIGHT },
    { 60, UP_RIGHT },
    { 120, UP },
    { 150, UP_LEFT },
    { 210, LEFT },
    { 240, DOWN_LEFT },
    { 300, DOWN },
    { 330, DOWN_RIGHT },
    { 360, RIGHT }
};

Direction direction(int angle)
{
    assert(angle >= 0 && angle <= 360);

    auto it = map.lower_bound(angle);
    return it->second;
}

int main()
{
    Direction d;

    d = direction(45);
    assert(d == UP_RIGHT);

    d = direction(30);
    assert(d == RIGHT);

    d = direction(360);
    assert(d == RIGHT);

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

  • @O.Jones:由编译时常量除法相当便宜,只是一个乘法和一些移位.我会选择其中一个`table [angle%360/30]的答案,因为它便宜且无分支.*远比树搜索循环便宜*,如果这个编译为与源类似的asm.(`的std :: unordered_map`通常是一个哈希表,但`的std :: map`通常红黑二叉树.接受的答案有效地使用`角度%360/30`作为角的完美散列函数(后复制几个条目,Bijay的答案甚至避免了偏移)). (17认同)
  • 你可以在排序的数组上使用`lower_bound`.这比`map`效率更高. (2认同)

dbu*_*ush 57

创建一个数组,其每个元素与一个30度的块相关联:

Car::EDirection dirlist[] = { 
    Car::EDirection::RIGHT, 
    Car::EDirection::UP_RIGHT, 
    Car::EDirection::UP, 
    Car::EDirection::UP, 
    Car::EDirection::UP_LEFT, 
    Car::EDirection::LEFT, 
    Car::EDirection::LEFT, 
    Car::EDirection::DOWN_LEFT,
    Car::EDirection::DOWN, 
    Car::EDirection::DOWN, 
    Car::EDirection::DOWN_RIGHT, 
    Car::EDirection::RIGHT
};
Run Code Online (Sandbox Code Playgroud)

然后你可以用角度/ 30索引数组:

this->_car.edir = dirlist[(this->_car.getAbsoluteAngle() % 360) / 30];
Run Code Online (Sandbox Code Playgroud)

无需比较或分支.

然而,结果略微偏离原始.边界上的值,即30,60,120等,放在下一个类别中.例如,在原始代码中,有效值为UP_RIGHT31到60.上面的代码分配30到59 UP_RIGHT.

我们可以通过从角度减去1来解决这个问题:

this->_car.edir = dirlist[((this->_car.getAbsoluteAngle() - 1) % 360) / 30];
Run Code Online (Sandbox Code Playgroud)

现在,这给了我们RIGHT30,UP_RIGHT60,等等.

在0的情况下,表达式变为(-1 % 360) / 30.这是有效的因为-1 % 360 == -1-1 / 30 == 0,所以我们仍然得到一个0的索引.

C++标准的 5.6节确认了这种行为:

4二元/运算符产生商,二元%运算符从第一个表达式除以第二个表达式得到余数.如果第二个操作数为/%为零,则行为未定义.对于积分操作数,/运算符产生代数商,丢弃任何小数部分.如果商a/b在结果类型中可表示, (a/b)*b + a%b则等于a.

编辑:

关于这样的构造的可读性和可维护性提出了许多问题.motoDrizzt给出的答案是简化原始结构的一个很好的例子,它更易于维护并且不那么"丑陋".

扩展他的答案,这是另一个利用三元运算符的例子.由于原始帖子中的每个案例都分配给同一个变量,因此使用此运算符可以进一步提高可读性.

int angle = ((this->_car.getAbsoluteAngle() % 360) + 360) % 360;

this->_car.edir = (angle <= 30)  ?  Car::EDirection::RIGHT :
                  (angle <= 60)  ?  Car::EDirection::UP_RIGHT :
                  (angle <= 120) ?  Car::EDirection::UP :
                  (angle <= 150) ?  Car::EDirection::UP_LEFT :
                  (angle <= 210) ?  Car::EDirection::LEFT : 
                  (angle <= 240) ?  Car::EDirection::DOWN_LEFT :
                  (angle <= 300) ?  Car::EDirection::DOWN:  
                  (angle <= 330) ?  Car::EDirection::DOWN_RIGHT :
                                    Car::EDirection::RIGHT;
Run Code Online (Sandbox Code Playgroud)


mot*_*zzt 49

该代码并不丑陋,简单,实用,易读且易于理解.它将以它自己的方法被隔离,所以没有人必须在日常生活中处理它.并且万一有人必须检查它 - 可能是因为他正在调试你的应用程序以解决其他地方的问题 - 这很容易让他花两秒钟来理解代码及其作用.

如果我正在进行这样的调试,我很乐意花五分钟时间来尝试理解你的功能.在这方面,所有其他功能完全失败,因为它们改变了一个简单的,忘记它,无错误的例程,在一个复杂的混乱中,人们在调试时将被迫深入分析和测试.作为一名项目经理,我自己会因为开发人员采取简单的任务而不是将其实现为简单,无害的方式而感到不安,浪费时间将其实现为过于复杂的方式.只要想想你一直在思考它,然后来到SO询问,一切只是为了恶化维护和可读性.

也就是说,您的代码中存在一个常见错误,使其可读性降低,并且您可以轻松地进行一些改进:

int angle = this->_car.getAbsoluteAngle();

if (angle <= 30 || angle >= 330)
  return Car::EDirection::RIGHT;
else if (angle <= 60)
  return Car::EDirection::UP_RIGHT;
else if (angle <= 120)
  return Car::EDirection::UP;
else if (angle <= 150)
  return Car::EDirection::UP_LEFT;
else if (angle <= 210)
  return Car::EDirection::LEFT;
else if (angle <= 240)
  return Car::EDirection::DOWN_LEFT;
else if (angle <= 300)
  return Car::EDirection::DOWN;
else if (angle <= 330)
  return Car::EDirection::DOWN_RIGHT;
Run Code Online (Sandbox Code Playgroud)

把它放到一个方法中,将返回的值赋给对象,折叠方法,并在永恒的剩余时间内忘记它.

PS还有另一个错误超过330门槛,但我不知道你想怎么对待它,所以我根本没有解决它.


稍后更新

根据评论,如果有的话你甚至可以摆脱其他:

int angle = this->_car.getAbsoluteAngle();

if (angle <= 30 || angle >= 330)
  return Car::EDirection::RIGHT;

if (angle <= 60)
  return Car::EDirection::UP_RIGHT;

if (angle <= 120)
  return Car::EDirection::UP;

if (angle <= 150)
  return Car::EDirection::UP_LEFT;

if (angle <= 210)
  return Car::EDirection::LEFT;

if (angle <= 240)
  return Car::EDirection::DOWN_LEFT;

if (angle <= 300)
  return Car::EDirection::DOWN;

if (angle <= 330)
  return Car::EDirection::DOWN_RIGHT;
Run Code Online (Sandbox Code Playgroud)

我没有这样做,因为我觉得某一点只是一个自己的偏好问题,我的回答的范围是(并且是)对你对"代码的丑陋"的关注给出不同的观点.无论如何,正如我所说,有人在评论中指出了这一点,我认为展示它是有道理的.

  • @Ðann我完全不同意'else if`.我发现能够浏览一个代码块并看到它是一个决策树而不是一组无关的语句是有用的.是的,`return`或`break`对于编译器*在'return`之后不是必需的*,但它们对于看一眼代码的人是有用的. (10认同)
  • 如果你想走这条路,你至少应该摆脱不必要的`else if`,`if`就足够了. (8认同)
  • @Ð是是的,我同意这太可怕了.但是,这不是"其他"让你这样做的,它是一个糟糕的风格指南,不会将'else if`视为一个独特的陈述.我会一直使用大括号,但我永远不会像我在我的要点中所展示的那样编写代码. (7认同)
  • *"......在永恒的剩余时间里忘掉它."*只有在**没有变化的情况下才有效** (2认同)

Bij*_*ung 39

在伪代码中:

angle = (angle + 30) %360; // Offset by 30. 
Run Code Online (Sandbox Code Playgroud)

因此,我们有0-60,60-90,90-150,...作为类别.在每个90度的象限中,一个部分有60个,一个部分有30个.所以,现在:

i = angle / 90; // Figure out the quadrant. Could be 0, 1, 2, 3 

j = (angle - 90 * i) >= 60? 1: 0; // In the quardrant is it perfect (eg: RIGHT) or imperfect (eg: UP_RIGHT)?

index = i * 2 + j;
Run Code Online (Sandbox Code Playgroud)

在包含适当顺序的枚举的数组中使用索引.

  • 这很好,可能是这里最好的答案.如果最初的提问者看到他对枚举的使用后,他会发现他有一个案例,它只是被转换成一个数字!完全消除枚举并只是坚持方向整数可能对他的代码中的其他地方有意义,这个答案直接在那里得到你. (7认同)

Cal*_*eth 18

switch (this->_car.getAbsoluteAngle() / 30) // integer division
{
    case 0:
    case 11: this->_car.edir = Car::EDirection::RIGHT; break;
    case 1: this->_car.edir = Car::EDirection::UP_RIGHT; break;
    ...
    case 10: this->_car.edir = Car::EDirection::DOWN_RIGHT; break;
}
Run Code Online (Sandbox Code Playgroud)

  • @BillK:如果编译器把它变成一个表查找,可能就是这样.这比使用if/else链更有可能.但由于它很简单并且不需要任何特定于体系结构的技巧,因此最好在源代码中编写表查找. (5认同)

Ðаn*_*Ðаn 16

忽略你的第if一个有点特殊情况,其余的都遵循完全相同的模式:最小值,最大值和方向; 伪代码:

if (angle > min && angle <= max)
  _car.edir = direction;
Run Code Online (Sandbox Code Playgroud)

制作这个真正的C++可能看起来像:

enum class EDirection {  NONE,
   RIGHT, UP_RIGHT, UP, UP_LEFT, LEFT, DOWN_LEFT, DOWN, DOWN_RIGHT };

struct AngleRange
{
    int min, max;
    EDirection direction;
};
Run Code Online (Sandbox Code Playgroud)

现在,而不是写一堆ifs,只是循环你的各种可能性:

EDirection direction_from_angle(int angle, const std::vector<AngleRange>& angleRanges)
{
    for (auto&& angleRange : angleRanges)
    {
        if ((angle > angleRange.min) && (angle <= angleRange.max))
            return angleRange.direction;
    }

    return EDirection::NONE;
}
Run Code Online (Sandbox Code Playgroud)

(throw荷兰国际集团的例外,而不是returnING NONE是另一种选择).

然后你会打电话给:

_car.edir = direction_from_angle(_car.getAbsoluteAngle(), {
    {30, 60, EDirection::UP_RIGHT},
    {60, 120, EDirection::UP},
    // ... etc.
});
Run Code Online (Sandbox Code Playgroud)

这种技术称为数据驱动编程.除了摆脱一堆ifs之外,它还允许您轻松添加更多方向(例如,NNW)或减少数量(左,右,上,下)而无需重新编写其他代码.


(处理你的第一个特殊情况留作"读者练习".:-))


x4u*_*x4u 12

尽管基于查找表的所提出的变体angle / 30可能是优选的,但是这里是使用硬编码二进制搜索来最小化比较次数的替代方案.

static Car::EDirection directionFromAngle( int angle )
{
    if( angle <= 210 )
    {
        if( angle > 120 )
            return angle > 150 ? Car::EDirection::LEFT
                               : Car::EDirection::UP_LEFT;
        if( angle > 30 )
            return angle > 60 ? Car::EDirection::UP
                              : Car::EDirection::UP_RIGHT;
    }
    else // > 210
    {
        if( angle <= 300 )
            return angle > 240 ? Car::EDirection::DOWN
                               : Car::EDirection::DOWN_LEFT;
        if( angle <= 330 )
            return Car::EDirection::DOWN_RIGHT;
    }
    return Car::EDirection::RIGHT; // <= 30 || > 330
}
Run Code Online (Sandbox Code Playgroud)