7.5 – Проваливание и область видимости в switch

Добавлено 25 мая 2021 в 23:39

Этот урок является продолжением нашего исследования операторов switch, которое мы начали в предыдущем уроке «7.4 – Основы работы с оператором switch». В предыдущем уроке мы упоминали, что каждый набор инструкций под меткой должен заканчиваться инструкцией break или инструкцией return.

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

Проваливание

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

  1. будет достигнут конец блока switch;
  2. другая инструкция управления порядком выполнения программы (обычно break или return) вызовет выход из блока switch или из функции;
  3. что-то еще прервет нормальный порядок выполнения программы (например, ОС закроет программу, вселенная взорвется и т.д.).

Обратите внимание, что наличие другой метки case не является одним из этих условий завершения – таким образом, без break или return выполнение будет переходить в последующие метки.

Вот программа, которая демонстрирует такое поведение:

#include <iostream>
 
int main()
{
    switch (2)
    {
    case 1: // Не совпадает
        std::cout << 1 << '\n'; // Пропущено
    case 2: // Совпадение!
        std::cout << 2 << '\n'; // Выполнение начинается здесь
    case 3:
        std::cout << 3 << '\n'; // Это тоже выполняется
    case 4:
        std::cout << 4 << '\n'; // Это тоже выполняется
    default:
        std::cout << 5 << '\n'; // Это тоже выполняется
    }
 
    return 0;
}

Эта программа выводит следующее:

2
3
4
5

Наверное, это не то, что мы хотели! Когда выполнение переходит от инструкции под меткой к ​​инструкциям под следующей меткой, это называется проваливанием.

Предупреждение


Как только инструкции под меткой case или default начнут выполняться, они будут переходить (проваливаться) в последующие метки. Для предотвращения этого обычно используются инструкции break или return.

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

Атрибут [[fallthrough]]

Комментирование преднамеренного проваливания – это обычное правило, чтобы сообщить другим разработчикам, что оно намеренное. Хотя это работает с другими разработчиками, компиляторы и инструменты анализа кода не умеют интерпретировать комментарии, поэтому это не позволяет избавиться от предупреждений.

Чтобы помочь решить эту проблему, C++17 добавил новый атрибут, называемый [[fallthrough]], который можно использовать в сочетании с пустой инструкцией, чтобы указать, что проваливание является преднамеренным (и предупреждения не должны для него выдаваться):

#include <iostream>
 
int main()
{
    switch (2)
    {
    case 1:
        std::cout << 1 << '\n';
        break;
    case 2:
        std::cout << 2 << '\n'; // Выполнение начинается здесь
        [[fallthrough]]; // преднамеренное проваливание - обратите внимание
                         // на точку с запятой, указывающую на пустую инструкцию
    case 3:
        std::cout << 3 << '\n'; // Это тоже выполняется
        break;
    }
 
    return 0;
}

Эта программа печатает:

2
3

И этот код не должен генерировать никаких предупреждений о проваливании.

Лучшая практика


Используйте атрибут [[fallthrough]] (вместе с пустой инструкцией), чтобы указать на преднамеренное проваливание.

Последовательные метки case

Чтобы объединить несколько проверок в одну инструкцию, в случае с операторами if вы можете использовать логический оператор ИЛИ:

bool isVowel(char c)
{
    return (c=='a' || c=='e' || c=='i' || c=='o' || c=='u' ||
        c=='A' || c=='E' || c=='I' || c=='O' || c=='U');
}

Этот подход страдает теми же проблемами, которые мы представили во введении к операторам switch: вычисление выполняется несколько раз, и читатель должен убедиться, что каждый раз вычисляется именно c.

Вы можете сделать что-то подобное, используя оператор switch, разместив несколько меток case последовательно:

bool isVowel(char c)
{
    switch (c)
    {
        case 'a': // если c равно 'a'
        case 'e': // или если c равно 'e'
        case 'i': // или если c равно 'i'
        case 'o': // или если c равно 'o'
        case 'u': // или если c равно 'u'
        case 'A': // или если c равно 'A'
        case 'E': // или если c равно 'E'
        case 'I': // или если c равно 'I'
        case 'O': // или если c равно 'O'
        case 'U': // или если c равно 'U'
            return true;
        default:
            return false;
    }
}

Помните, выполнение начинается с первой инструкции после соответствующей метки case. Метки case – это не инструкции, поэтому они не учитываются.

Первая инструкция после всех меток case в приведенной выше программе – return true;, поэтому, если есть совпадение с какой-либо меткой case, функция вернет true.

Таким образом, мы можем «складывать» метки case, чтобы впоследствии все эти метки case использовали один и тот же набор инструкций. Это не считается поведением проваливания, поэтому использование комментариев или [[fallthrough]] здесь не требуется.

Область видимости switch

С операторами if у вас может быть только одна инструкция после условия if, и эта инструкция неявно помещается внутрь блока:

if (x > 10)
    std::cout << x << " is greater than 10\n"; // эта строка неявно считается находящейся внутри блока

Однако с операторами switch все инструкции после меток относятся к блоку switch. Неявные блоки не создаются.

switch (1)
{
    case 1:
        foo();
        break;
    default:
        std::cout << "default case\n";
        break;
}

В приведенном выше примере 2 инструкции между case 1 и меткой default имеют область видимости как часть блока switch, а не как неявный блок для case 1.

Объявление и инициализация переменных внутри инструкций case

Вы можете объявлять (но не инициализировать) переменные внутри switch как до, так и после меток case:

switch (1)
{
    int a;      // хорошо: объявление разрешено перед метками case
    int b{ 5 }; // недопустимо: инициализация не разрешена до меток case
 
    case 1:
        int y; // хорошо, но это плохая практика: объявление разрешено внутри case
        y = 4; // хорошо: присвоение разрешено
        break;
 
    case 2:
        y = 5; // хорошо: y была объявлена выше, поэтому мы можем использовать ее и здесь
        break;
 
    case 3:
        int z{ 4 }; // недопустимо: инициализация не разрешена внутри case
        break;
}

Обратите внимание, что хотя переменная y была определена внутри case 1, она также использовалась внутри case 2. Поскольку инструкции в каждом case не находятся внутри неявных блоков, это означает, что все инструкции внутри switch являются частью одной и той же области видимости. Таким образом, переменная, определенная в одном case, может использоваться в более позднем case, даже если case, в котором определена переменная, никогда не выполняется! Другими словами, определение переменной без инициализатора просто сообщает компилятору, что с этого момента переменная находится в области видимости. Это не требует, чтобы определение действительно выполнялось.

Однако инициализация переменных запрещена и вызовет ошибку компиляции. Это связано с тем, что для инициализации переменной требуется выполнение, и инициализацию можно пропустить в зависимости от того, какие метки case выполняются.

Если для case необходимо определить и/или инициализировать новую переменную, лучше всего сделать это внутри блока под меткой case:

switch (1)
{
    case 1:
    { // обратите внимание на добавление здесь блока
        int x{ 4 }; // хорошо, переменные можно инициализировать внутри блока внутри case
        std::cout << x;
        break;
    }
    default:
        std::cout << "default case\n";
        break;
}

Правило


Если вы определяете переменные, используемые в case, делайте это в блоке внутри case (или перед switch, если необходимо)

Небольшой тест

Вопрос 1

Напишите функцию с именем calculate(), которая принимает два целых числа int и символ char, представляющий одну из следующих математических операций: +, -, *, / или % (остаток от деления). Используйте оператор switch, чтобы выполнить соответствующую математическую операцию с целыми числами и вернуть результат. Если в функцию передан недопустимый оператор, функция должна вывести ошибку. Для оператора деления выполните целочисленное деление.

Подсказка: "operator" – это ключевое слово, переменные не могут называться "operator".

#include <iostream>
 
int calculate(int x, int y, char op)
{
    switch (op)
    {
        case '+':
            return x + y;
        case '-':
            return x - y;
        case '*':
            return x * y;
        case '/':
            return x / y;
        case '%':
            return x % y;
        default:
            std::cout << "calculate(): Unhandled case\n";
            return 0;
    }
}
 
int main()
{
    std::cout << "Enter an integer: ";
    int x{};
    std::cin >> x;
 
    std::cout << "Enter another integer: ";
    int y{};
    std::cin >> y;
 
    std::cout << "Enter a mathematical operator (+, -, *, /, or %): ";
    char op{};
    std::cin >> op;
 
    std::cout << x << ' ' << op << ' ' << y << " is " << calculate(x, y, op) << '\n';
 
    return 0;
}

Теги

[[fallthrough]]C++ / CppLearnCppswitchДля начинающихОбучениеПрограммированиеУсловный оператор

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.