Switch для двух параметров в С++

в 10:13, , рубрики: c++, constexpr, static_assert, switch, Песочница, Программирование, метки: , , ,

Читая посты на Хабре, наткнулся на такой вопрос. В комментариях были предложены решения, но ни одно не подходило автору в виду накладных расходов на вызовы функций. И тогда я задумался, а действительно почему бы не использовать обычный switch, рассчитывая из двух параметров один хеш, который и использовать в switch. Но глянув на пример автора вопроса внимательнее я понял, что такой вариант просто так не пройдет, так как надо отлавливать default вложенных switch'ей.

Допустим есть две переменные n и m, каждая может принимать значение от 0 до 9, и мы имеем такую структуру:

switch (n)
{
    case 0:
    {
        switch (m)
        {
            case 2:
            case 4: ... break;
            case 5: ... break;
            default: ... break;
        }
    }
    break;
    ...
}

Так как значение каждой переменной помещается в байт, то пусть хеш функция будет следующей ((n << SHIFT) + m) (где SHIFT = 8).
И вот тут то и возникает проблема: если определены оба значения, то мы получим конкретное число после расчета хеша. Но если скажем n = 0, а m не равно 2,4,5 то что делать в этом случае? Перечислять остальные варианты для выполнения действий по умолчанию слишком накладно, ведь их диапазоны значений могут быть куда больше чем 0..9. То есть по сути надо словить попадание значения вычисленного хеша в некоторый диапазон.

И тогда я и подумал, а что если перечислить сами default'ы. То есть сначала написать case'ы для всех пар m и n, для которых надо выполнить некоторые действия, а затем в default объявить вложенный switch с одним параметром n, таким образом будут перебраны все остальные комбинации, которые надо обработать.

Итак что же у меня получилось

Такую простую хеш функцию можно было бы реализовать обычным макросом, но так как в С++11 появились constexpr функции, то решил использовать их:

constexpr int hash(int n, int m)
{
    return verifyValues(n, m) ? ((n << SHIFT) + m): -1;
}

, verifyValues это функция для проверки, что параметры лежат в заданном диапазоне. Для этого используются константы MAX_N, MAX_M, и в случае если параметры не валидны будет возвращено -1.

#define isInBound(min, value, max) ((value >= min) && (value <= max))

constexpr bool verifyValues(int n, int m)
{
    return isInBound(0, n, MAX_N) && isInBound(0, m, MAX_M);
}

Глядя на хеш функцию также хорошо было бы добавить следующую проверку:

MAX_M < pow(2, SHIFT)

Дополнительный код и проверки скроем с помощью макросов:

#define SWITCH(n, m) static_assert(MAX_M < pow(2, SHIFT), "shift value is not enough to cover all M values");   
   switch(hash(n, m))

#define CASE(n, m) static_assert(verifyValues(n, m), "N or M value is out of range");   
   case hash(n, m)

Здесь при определении switch'а будет сразу проверена работоспособность нашей хеш функции, а в дальнейшем для каждого case'а будет проверено, что его переменные находятся в заданных диапазонах.

Итак с первой частью разобрались, теперь собственно секция default.
Здесь можно было бы просто прописать вложенный switch, но коль начал писать макросы, то определю и для него:

#define DEFAULT(n)                          
    case -1:    ASSERT(false);      break;  
    default: switch(n) {

#define DEFAULT_CASE(n) case n

#define END_DEFAULT }

Так как при расчете хеш функция может вернуть -1 (в случае передаче ей неверных параметров), то был добавлен обработчик для case -1 (в моем примере это обычный ASSERT).

В итоге у меня получился вот такой switch с двумя параметрами:

SWITCH(a, b)
{
    CASE(0, 1):
    CASE(0, 2):
    CASE(0, 3):
    CASE(0, 4):                ...    break;
    CASE(5, 3):                ...    break;

    DEFAULT(a)
        DEFAULT_CASE(0):    ...    break;
        DEFAULT_CASE(1):    ...    break;
        DEFAULT_CASE(2):    ...    break;
        DEFAULT_CASE(3):    ...    break;
        DEFAULT_CASE(4):    ...    break;
        DEFAULT_CASE(5):    ...    break;
        DEFAULT_CASE(6):    ...    break;
        DEFAULT_CASE(7):    ...    break;
        DEFAULT_CASE(8):    ...    break;
        DEFAULT_CASE(9):    ...    break;
    END_DEFAULT
}

Тут главное не забыть, что после макроса DEFAULT не надо ставить двоеточие.

Весь код целиком

const int MAX_N = 10;
const int MAX_M = 10;
const int SHIFT = 8;

#define isInBound(min, value, max) ((value >= min) && (value <= max))

constexpr bool verifyValues(int n, int m)
{
    return isInBound(0, n, MAX_N) && isInBound(0, m, MAX_M);
}

constexpr int hash(int n, int m)
{
    return verifyValues(n, m) ? ((n << SHIFT) + m): -1;
}

#define SWITCH(n, m) static_assert(MAX_M < pow(2, SHIFT), "shift value is not enough to cover all M values");   
   switch(hash(n, m))

#define CASE(n, m) static_assert(verifyValues(n, m), "N or M value is out of range");   
   case hash(n, m)

#define DEFAULT(n)                          
    case -1:    Q_ASSERT(false);      break;  
    default: switch(n) {

#define DEFAULT_CASE(n) case n

#define END_DEFAULT }

...

SWITCH(a, b)
{
    CASE(0, 1):
    CASE(0, 2):
    CASE(0, 3):
    CASE(0, 4):             printf("0, 1-4n"); break;
    CASE(5, 3):             printf("5, 3n");   break;

    DEFAULT(a)
        DEFAULT_CASE(0):    printf("0n");      break;
        DEFAULT_CASE(1):    printf("1n");      break;
        DEFAULT_CASE(2):    printf("2n");      break;
        DEFAULT_CASE(3):    printf("3n");      break;
        DEFAULT_CASE(4):    printf("4n");      break;
        DEFAULT_CASE(5):    printf("5n");      break;
        DEFAULT_CASE(6):    printf("6n");      break;
        DEFAULT_CASE(7):    printf("7n");      break;
        DEFAULT_CASE(8):    printf("8n");      break;
        DEFAULT_CASE(9):    printf("9n");      break;
    END_DEFAULT
}

В итоге имеем switch, который работает с двумя параметрами. Из накладных расходов элементарная хеш функция. Но в тоже время для case'ов, для который определены оба параметра переход будет осуществлен уже в первом switch'е, а для default как и раньше во втором.

Надеюсь, что кому-нибудь эта реализация пригодится. Спасибо за внимание.

P.S. В принципе используя такую структуру можно написать switch не только для двух параметров, но и для трех и более.

Автор: NTSky

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js