Как избежать цепочки if / else if при классификации заголовка по 8 направлениям?

111

У меня такой код:

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;

Я хочу избежать ifцепочки s; это действительно некрасиво. Есть ли другой, возможно, более чистый способ написать это?

Oraekia
источник
77
@Oraekia Это выглядело бы намного менее уродливо, меньше печатать и лучше читать, если вы испытаете this->_car.getAbsoluteAngle()один раз перед всем каскадом.
πάντα ῥεῖ
26
Все это явное разыменование this( this->) не нужно и на самом деле ничего хорошего для читабельности не делает ..
Джеспер Джул
2
@Neil Пара как ключ, перечисление как значение, настраиваемая лямбда поиска.
πάντα ῥεῖ
56
Без всех этих >тестов код был бы намного менее уродливым ; они не нужны, поскольку каждый из них уже был протестирован (в обратном направлении) в предыдущем ifутверждении.
Пит Беккер
10
@PeteBecker Это одна из моих любимых мозолей по поводу такого кода. Слишком много программистов не понимают else if.
Barmar

Ответы:

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

Вот как бы я это сделал. (Согласно моему предыдущему комментарию).

Borgleader
источник
92
Мне буквально на секунду показалось, что я вижу код Konami вместо перечисления.
Зано
21
@CodesInChaos: C99 и C ++ имеют то же требование, что и C #: если if q = a/bи r = a%bthen q * b + rдолжны быть равны a. Таким образом, в C99 допустимо, чтобы остаток был отрицательным. BorgLeader, вы можете решить проблему с помощью (((angle % 360) + 360) % 360) / 30.
Эрик Липперт
7
@ericlippert, вы и ваши знания в области вычислительной математики продолжаете впечатлять.
gregsdennis
33
Это очень умно, но совершенно нечитаемо и маловероятно, что его можно будет сопровождать, поэтому я не согласен с тем, что это хорошее решение воспринимаемой «уродливости» оригинала. Я думаю, здесь есть элемент личного вкуса, но я считаю, что очищенные версии ветвления от x4u и motoDrizzt намного предпочтительнее.
IMSoP
4
@cyanbeam цикл for в main - это просто «демонстрация», GetDirectionForAngleэто то, что я предлагаю в качестве замены для каскада if / else, они оба O (1) ...
Borgleader
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;
}
Стив Лоример
источник
Деление не требуется. Хорошо!
О. Джонс
17
@ O.Jones: Деление на константу времени компиляции довольно дешево, просто умножение и некоторые сдвиги. Я бы выбрал один из table[angle%360/30]ответов, потому что он дешевый и безотказный. Намного дешевле, чем цикл поиска по дереву, если он компилируется в asm, похожий на исходный. ( std::unordered_mapобычно представляет собой хеш-таблицу, но std::mapобычно представляет собой красно-черное двоичное дерево. Принятый ответ эффективно используется angle%360 / 30в качестве идеальной хеш-функции для углов (после репликации пары записей, а ответ Биджея даже позволяет избежать этого со смещением)).
Питер Кордес
2
Вы можете использовать lower_boundотсортированный массив. Это было бы намного эффективнее, чем map.
Wilx
Поиск по карте @PeterCordes легко написать и легко поддерживать. Если диапазоны меняются, обновление хеш-кода может привести к ошибкам, а если диапазоны станут неоднородными, он может просто развалиться. Я бы не стал беспокоиться, если этот код не критичен к производительности.
OhJeez
@OhJeez: они уже неоднородны, что обрабатывается за счет того, что одно и то же значение в нескольких сегментах. Просто используйте меньший делитель, чтобы получить больше сегментов, если это не означает использование очень маленького делителя и слишком много сегментов. Кроме того, если производительность не имеет значения, тогда цепочка if / else тоже неплохая, если ее упростить путем выделения this->_car.getAbsoluteAngle()с помощью tmp var и удаления избыточного сравнения из каждого предложения OP if()(проверка того, что уже соответствовало предыдущий if ()). Или используйте предложение отсортированного массива @ wilx.
Питер Кордес
58

Создайте массив, каждый элемент которого связан с блоком 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
};

Затем вы можете проиндексировать массив с углом / 30:

this->_car.edir = dirlist[(this->_car.getAbsoluteAngle() % 360) / 30];

Никаких сравнений или ветвлений не требуется.

Однако результат немного отличается от оригинала. Значения на границах, то есть 30, 60, 120 и т. Д., Помещаются в следующую категорию. Например, в исходном коде допустимые значения от UP_RIGHT31 до 60. Приведенный выше код присваивает от 30 до 59 UP_RIGHT.

Мы можем обойти это, вычтя 1 из угла:

this->_car.edir = dirlist[((this->_car.getAbsoluteAngle() - 1) % 360) / 30];

Теперь мы получаем RIGHT30, UP_RIGHT60 и т. Д.

В случае 0 выражение становится (-1 % 360) / 30. Это верно, потому что -1 % 360 == -1и -1 / 30 == 0, поэтому мы все еще получаем индекс 0.

Раздел 5.6 стандарта C ++ подтверждает это поведение:

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;
dbush
источник
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;

Поместите это в метод, назначьте возвращаемое значение объекту, сверните метод и забудьте о нем на всю оставшуюся вечность.

PS есть еще одна ошибка, превышающая порог 330, но я не знаю, как вы хотите ее лечить, поэтому я ее вообще не исправил.


Позднее обновление

Согласно комментарию, вы даже можете избавиться от else, если вообще:

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;

Я не делал этого, потому что чувствую, что в какой-то момент это становится просто вопросом личных предпочтений, и объем моего ответа заключался (и есть) в том, чтобы дать другую точку зрения на вашу озабоченность «уродством кода». Во всяком случае, как я уже сказал, кто-то указал на это в комментариях, и я думаю, что есть смысл показать это.

motoDrizzt
источник
1
Что, черт возьми, это должно было достичь?
Абстракция - это все.
8
Если вы хотите пройти по этому маршруту, вы должны хотя бы избавиться от ненужного else if, этого ifдостаточно.
Ðаn
10
@ Ðаn Я полностью не согласен с else if. Я считаю полезным иметь возможность взглянуть на блок кода и увидеть, что это дерево решений, а не группа не связанных между собой операторов. Да, elseили breakони не нужны компилятору после a return, но они полезны для человека, который смотрит на код.
IMSoP
@ Ðаn Я никогда не видел языка, где требовалось бы дополнительное вложение. Либо есть отдельное ключевое слово elseif/ elsif, либо вы технически используете блок из одного оператора, который начинается if, как здесь. Быстрый пример того, о чем я думаю, и о чем я думаю: gist.github.com/IMSoP/90bc1e9e2c56d8314413d7347e76532a
IMSoP
7
@ Ðаn Да, я согласен, это было бы ужасно. Но это не elseзаставляет вас делать это, это плохое руководство по стилю, которое не распознается else ifкак отдельное утверждение. Я всегда использовал фигурные скобки, но я никогда не стал бы писать такой код, как я показал в своей сути.
IMSoP
39

В псевдокоде:

angle = (angle + 30) %360; // Offset by 30. 

Итак, у нас есть 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;

Используйте индекс в массиве, содержащем перечисления в соответствующем порядке.

Биджай Гурунг
источник
7
Это хороший, наверное, лучший ответ здесь. Скорее всего, если бы первоначальный вопросник позже посмотрел на свое использование перечисления, он обнаружил бы, что у него есть случай, когда оно просто конвертируется обратно в число! Полное исключение перечисления и простое использование целого числа направления, вероятно, имеет смысл в других местах его кода, и этот ответ приведет вас прямо туда.
Bill K
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;
}
Caleth
источник
угол = это -> _ car.getAbsoluteAngle (); сектор = (угол% 360) / 30; Результат - 12 секторов. Затем выполните индексирование в массив или используйте переключатель / регистр, как указано выше - компилятор все равно преобразует в таблицу переходов.
ChuckCottrill
1
Переключиться не особо лучше, чем цепочки if / else.
Bill K
5
@BillK: Может быть, если компилятор превратит его в поиск по таблице для вас. Это более вероятно, чем с цепочкой if / else. Но поскольку это просто и не требует каких-либо специфических для архитектуры уловок, вероятно, лучше всего написать поиск по таблице в исходном коде.
Питер Кордес
Как правило, производительность не должна быть проблемой - это читабельность и ремонтопригодность - каждая цепочка switch и if / else обычно означает кучу беспорядочного кода копирования и вставки, который необходимо обновлять в нескольких местах всякий раз, когда вы добавляете новый элемент. Лучше избегать того и другого и попробовать распределить таблицы, вычисления или просто загрузить данные из файла и, если можете, рассматривать их как данные.
Bill K
Компилятор PeterCordes, вероятно, сгенерирует идентичный код для LUT, что и для коммутатора. @BillK, вы можете извлечь переключатель в функцию 0..12 -> Car :: EDirection, которая будет иметь эквивалентное повторение LUT
Caleth
16

Игнорируя ваше первое, ifчто является немного частным случаем, все остальные следуют одному и тому же шаблону: min, max и direction; псевдокод:

if (angle > min && angle <= max)
  _car.edir = direction;

Реальный C ++ может выглядеть так:

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

struct AngleRange
{
    int min, max;
    EDirection direction;
};

Теперь, вместо того, чтобы писать кучу 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;
}

( throwИНГ исключение , а не returnИнг NONEдругой вариант).

Что бы вы затем назвали:

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

Этот метод известен как программирование, управляемое данными . Помимо избавления от множества ifs, это позволит вам легко использовать добавление дополнительных направлений (например, NNW) или уменьшать число (влево, вправо, вверх, вниз) без переделки другого кода.


(Работа с вашим первым частным случаем остается «упражнением для читателя». :-))

Ðаn
источник
1
Технически вы можете исключить min, учитывая, что все диапазоны углов совпадают, что уменьшит условие до if(angle <= angleRange.max)+1 для использования таких функций C ++ 11, как enum class.
Pharap
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
}
x4u
источник
2

Если вы действительно хотите избежать дублирования, вы можете выразить это в виде математической формулы.

Прежде всего, предположим, что мы используем Enum от @ Geek.

Enum EDirection { RIGHT =0, UP_RIGHT, UP, UP_LEFT, LEFT, DOWN_LEFT,DOWN, DOWN_RIGHT}

Теперь мы можем вычислить перечисление, используя целочисленную математику (без необходимости в массивах).

EDirection angle2dir(int angle) {
    int d = ( ((angle%360)+360)%360-1)/30;
    d-=d/3; //some directions cover a 60 degree arc
    d%=8;
    //printf ("assert(angle2dir(%3d)==%-10s);\n",angle,dir2str[d]);
    return (EDirection) d;
}

Как указывает @motoDrizzt, краткий код не обязательно читаемый. У него есть небольшое преимущество в том, что его математическое выражение явно указывает на то, что некоторые направления покрывают более широкую дугу. Если вы хотите пойти в этом направлении, вы можете добавить несколько утверждений, которые помогут понять код.

assert(angle2dir(  0)==RIGHT     ); assert(angle2dir( 30)==RIGHT     );
assert(angle2dir( 31)==UP_RIGHT  ); assert(angle2dir( 60)==UP_RIGHT  );
assert(angle2dir( 61)==UP        ); assert(angle2dir(120)==UP        );
assert(angle2dir(121)==UP_LEFT   ); assert(angle2dir(150)==UP_LEFT   );
assert(angle2dir(151)==LEFT      ); assert(angle2dir(210)==LEFT      );
assert(angle2dir(211)==DOWN_LEFT ); assert(angle2dir(240)==DOWN_LEFT );
assert(angle2dir(241)==DOWN      ); assert(angle2dir(300)==DOWN      );
assert(angle2dir(301)==DOWN_RIGHT); assert(angle2dir(330)==DOWN_RIGHT);
assert(angle2dir(331)==RIGHT     ); assert(angle2dir(360)==RIGHT     );

Добавив утверждения, вы добавили дублирование, но дублирование в утверждениях не так уж и плохо. Если у вас есть непоследовательное утверждение, вы скоро узнаете. Утверждения могут быть скомпилированы вне версии выпуска, чтобы не раздувать исполняемый файл, который вы распространяете. Тем не менее, этот подход, вероятно, наиболее применим, если вы хотите оптимизировать код, а не просто сделать его менее уродливым.

gmatht
источник
1

Я опаздываю на вечеринку, но мы могли бы использовать флаги перечисления и проверки диапазона, чтобы сделать что-нибудь аккуратное.

enum EDirection {
    RIGHT =  0x01,
    LEFT  =  0x02,
    UP    =  0x04,
    DOWN  =  0x08,
    DOWN_RIGHT = DOWN | RIGHT,
    DOWN_LEFT = DOWN | LEFT,
    UP_RIGHT = UP | RIGHT,
    UP_LEFT = UP | LEFT,

    // just so we be clear, these won't have much use though
    IMPOSSIBLE_H = RIGHT | LEFT, 
    IMPOSSIBLE_V = UP | DOWN
};

проверка (псевдокод), предполагая абсолютный угол (от 0 до 360):

int up    = (angle >   30 && angle <  150) * EDirection.UP;
int down  = (angle >  210 && angle <  330) * EDirection.DOWN;
int right = (angle <=  60 || angle >= 330) * EDirection.Right;
int left  = (angle >= 120 && angle <= 240) * EDirection.LEFT;

EDirection direction = (Direction)(up | down | right | left);

switch(direction){
    case RIGHT:
         // do right
         break;
    case UP_RIGHT:
         // be honest
         break;
    case UP:
         // whats up
         break;
    case UP_LEFT:
         // do you even left
         break;
    case LEFT:
         // 5 done 3 to go
         break;
    case DOWN_LEFT:
         // your're driving me to a corner here
         break;
    case DOWN:
         // :(
         break;
    case DOWN_RIGHT:
         // completion
         break;

    // hey, we mustn't let these slide
    case IMPOSSIBLE_H:
    case IMPOSSIBLE_V:
        // treat your impossible case here!
        break;
}
Леонардо Пина
источник