Сделайте так, чтобы компилятор предполагал, что все случаи обрабатываются в switch без значения по умолчанию - PullRequest
0 голосов
/ 11 января 2019

Давайте начнем с некоторого кода. Это чрезвычайно упрощенная версия моей программы.

#include <stdint.h>

volatile uint16_t dummyColorRecepient;

void updateColor(const uint8_t iteration)
{
    uint16_t colorData;
    switch(iteration)
    {
    case 0:
        colorData = 123;
        break;
    case 1:
        colorData = 234;
        break;
    case 2:
        colorData = 345;
        break;
    }
    dummyColorRecepient = colorData;
}

// dummy main function
int main()
{
    uint8_t iteration = 0;
    while (true)
    {
        updateColor(iteration);
        if (++iteration == 3)
            iteration = 0;
    }
}

Программа компилируется с предупреждением:

./test.cpp: In function ‘void updateColor(uint8_t)’:
./test.cpp:20:25: warning: ‘colorData’ may be used uninitialized in this function [-Wmaybe-uninitialized]
     dummyColorRecepient = colorData;
     ~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~

Как видите, существует абсолютная уверенность, что переменная iteration всегда равна 0, 1 или 2. Однако компилятор этого не знает и предполагает, что switch может не инициализировать colorData. (Любой объем статического анализа во время компиляции здесь не поможет, потому что настоящая программа распределена по нескольким файлам.)

Конечно, я мог бы просто добавить оператор по умолчанию, например default: colorData = 0;, но это добавляет в программу дополнительные 24 байта. Это программа для микроконтроллера, и у меня очень строгие ограничения на ее размер.

Я хотел бы сообщить компилятору, что этот переключатель гарантированно охватывает все возможные значения iteration.

Ответы [ 5 ]

0 голосов
/ 13 января 2019

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

void updateColor(const uint8_t iteration)
{
    dummyColorRecepient = 123 + 111 * iteration;
}
0 голосов
/ 11 января 2019

Я знаю, что было несколько хороших решений, но в качестве альтернативы If ваши значения будут известны во время компиляции, вместо оператора switch вы можете использовать constexpr с static function template и парой enumerators; это будет выглядеть примерно так в одном классе:

#include <iostream>

class ColorInfo {
public:
    enum ColorRecipient {
        CR_0 = 0,
        CR_1,
        CR_2
    };

    enum ColorType {
        CT_0 = 123,
        CT_1 = 234,
        CT_2 = 345
    };

    template<const uint8_t Iter>
    static constexpr uint16_t updateColor() {

        if constexpr (Iter == CR_0) {
            std::cout << "ColorData updated to: " << CT_0 << '\n';
            return CT_0;
        }

        if constexpr (Iter == CR_1) {
            std::cout << "ColorData updated to: " << CT_1 << '\n';
            return CT_1;
        }

        if constexpr (Iter == CR_2) {
            std::cout << "ColorData updated to: " << CT_2 << '\n';
            return CT_2;
        }
    }
};

int main() {
    const uint16_t colorRecipient0 = ColorInfo::updateColor<ColorInfo::CR_0>();
    const uint16_t colorRecipient1 = ColorInfo::updateColor<ColorInfo::CR_1>();
    const uint16_t colorRecipient2 = ColorInfo::updateColor<ColorInfo::CR_2>();

    std::cout << "\n--------------------------------\n";
    std::cout << "Recipient0: " << colorRecipient0 << '\n'
              << "Recipient1: " << colorRecipient1 << '\n'
              << "Recipient2: " << colorRecipient2 << '\n';

    return 0;
}

Операторы cout в if constexpr добавляются только для целей тестирования, но это должно проиллюстрировать еще один возможный способ сделать это без использования оператора switch, если ваши значения будут известны во время компиляции. Если эти значения генерируются во время выполнения, я не совсем уверен, есть ли способ использовать constexpr для достижения такого типа структуры кода, но если есть, я был бы признателен, если кто-то еще с немного большим опытом мог бы разработать о том, как это можно сделать с constexpr, используя runtime значения. Тем не менее, этот код очень удобочитаем, поскольку в нем нет magic numbers, а код достаточно выразителен.


- Обновление -

Прочитав больше о constexpr, я понял, что их можно использовать для генерации compile time constants. Я также узнал, что они не могут генерировать runtime constants, но они могут быть использованы в runtime function. Мы можем взять описанную выше структуру класса и использовать ее в функции времени выполнения как таковой, добавив static function в класс:

static uint16_t colorUpdater(const uint8_t input) {
    // Don't forget to offset input due to std::cin with ASCII value.
    if ( (input - '0') == CR_0)
        return updateColor<CR_0>();
    if ( (input - '0') == CR_1)
        return updateColor<CR_1>();
    if ( (input - '0') == CR_2)
        return updateColor<CR_2>();

    return updateColor<CR_2>(); // Return the default type
}

Однако я хочу изменить соглашения об именах двух функций. Первую функцию я назову colorUpdater(), а эту новую функцию, которую я только что показал выше, я назову updateColor(), так как она кажется более интуитивно понятной. Таким образом, обновленный класс теперь будет выглядеть так:

class ColorInfo {
public:
    enum ColorRecipient {
        CR_0 = 0,
        CR_1,
        CR_2
    };

    enum ColorType {
        CT_0 = 123,
        CT_1 = 234,
        CT_2 = 345
    };

    static uint16_t updateColor(uint8_t input) {
        if ( (input - '0') == CR_0 ) {
            return colorUpdater<CR_0>();
        }
        if ( (input - '0') == CR_1 ) {
            return colorUpdater<CR_1>();
        }
        if ( (input - '0') == CR_2 ) {
            return colorUpdater<CR_2>();
        }

        return colorUpdater<CR_0>(); // Return the default type
    }

    template<const uint8_t Iter>
    static constexpr uint16_t colorUpdater() {

        if constexpr (Iter == CR_0) {
            std::cout << "ColorData updated to: " << CT_0 << '\n';
            return CT_0;
        }

        if constexpr (Iter == CR_1) {
            std::cout << "ColorData updated to: " << CT_1 << '\n';
            return CT_1;
        }

        if constexpr (Iter == CR_2) {
            std::cout << "ColorData updated to: " << CT_2 << '\n';
            return CT_2;
        }
    }
};

Если вы хотите использовать это только с константами времени компиляции, вы можете использовать его так же, как и раньше, но с обновленным именем функции.

#include <iostream>

int main() {
    auto output0 = ColorInfo::colorUpdater<ColorInfo::CR_0>();
    auto output1 = ColorInfo::colorUpdater<ColorInfo::CR_1>();
    auto output2 = ColorInfo::colorUpdater<ColorInfo::CR_2>();

    std::cout << "\n--------------------------------\n";
    std::cout << "Recipient0: " << output0 << '\n'
              << "Recipient1: " << output1 << '\n'
              << "Recipient2: " << output2 << '\n';
    return 0;
}

И если вы хотите использовать этот механизм со значениями runtime, вы можете просто сделать следующее:

int main() {
    uint8_t input;
    std::cout << "Please enter input value [0,2]\n";
    std::cin >> input;

    auto output = ColorInfo::updateColor(input);

    std::cout << "Output: " << output << '\n';

    return 0;
}

И это будет работать со значениями времени выполнения.

0 голосов
/ 11 января 2019

Как видите, существует абсолютная уверенность, что переменная итерации всегда равна 0, 1 или 2.

С точки зрения цепочки инструментов, это не так. Вы можете вызвать эту функцию из другого места, даже из другого модуля перевода. Единственное место, где применяется ваше ограничение, находится в main, и даже там оно сделано таким образом, что компилятору может быть трудно рассуждать.

Для наших целей, однако, давайте прочитаем, что вы не собираетесь связывать какие-либо другие единицы перевода, и что мы хотим рассказать об этом инструментарию. Ну, к счастью, мы можем!

Если вы не возражаете против того, чтобы быть непереносимым, то есть GCC __builtin_unreachable встроенный , чтобы сообщить ему, что дело default не ожидается, и его следует считать недостижимым. Мой GCC достаточно умен, чтобы знать, что это означает, что colorData никогда не останется неинициализированным, пока все ставки не будут отменены.

#include <stdint.h>

volatile uint16_t dummyColorRecepient;

void updateColor(const uint8_t iteration)
{
    uint16_t colorData;
    switch(iteration)
    {
    case 0:
        colorData = 123;
        break;
    case 1:
        colorData = 234;
        break;
    case 2:
        colorData = 345;
        break;

    // Comment out this default case to get the warnings back!
    default:
        __builtin_unreachable();
    }
    dummyColorRecepient = colorData;
}

// dummy main function
int main()
{
    uint8_t iteration = 0;
    while (true)
    {
        updateColor(iteration);
        if (++iteration == 3)
            iteration = 0;
    }
}

( демоверсия )

Это не добавит фактическую ветку default, потому что внутри нее нет «кода». Фактически, когда я подключил это к Godbolt, используя x86_64 GCC с -O2, программа была меньше с этим дополнением, чем без него & mdash; логически, вы только что добавили крупную подсказку по оптимизации.

На самом деле есть предложение сделать его стандартным атрибутом в C ++ , чтобы в будущем это могло быть еще более привлекательным решением.

0 голосов
/ 11 января 2019

Вы можете получить это для компиляции без предупреждений, просто добавив метку default в один из случаев:

switch(iteration)
{
case 0:
    colorData = 123;
    break;
case 1:
    colorData = 234;
    break;
case 2: default:
    colorData = 345;
    break;
}

В качестве альтернативы:

uint16_t colorData = 345;
switch(iteration)
{
case 0:
    colorData = 123;
    break;
case 1:
    colorData = 234;
    break;
}

Попробуйте оба варианта и используйте меньшее из двух.

0 голосов
/ 11 января 2019

Используйте идиому «немедленно вызванное лямбда-выражение» и assert:

void updateColor(const uint8_t iteration)
{
    const auto colorData = [&]() -> uint16_t
    {
        switch(iteration)
        {
            case 0: return 123;
            case 1: return 234;
        }

        assert(iteration == 2);
        return 345;
    }();

    dummyColorRecepient = colorData;
}
  • Лямбда-выражение позволяет пометить colorData как const. const переменные всегда должны быть инициализированы.

  • Комбинация операторов assert + return позволяет вам избегать предупреждений и обрабатывать все возможные случаи.

  • assert не компилируется в режиме выпуска, что исключает накладные расходы.


Вы также можете выделить функцию:

uint16_t getColorData(const uint8_t iteration)
{
    switch(iteration)
    {
        case 0: return 123;
        case 1: return 234;
    }

    assert(iteration == 2);
    return 345;
}

void updateColor(const uint8_t iteration)
{
    const uint16_t colorData = getColorData(iteration);
    dummyColorRecepient = colorData;
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...