Недавно читали код и наткнулись на примерно такой такой фрагмент.

bool *t = new bool[X][Y];
// много строк
switch (t[M][N])
{
case true:
        // много строк
        break;
case false:
        // много строк
        break;
default:
        // много строк
        break;
}

Сразу возникает вопрос: зачем нужна ветка default? Если переменная не равна true, то она равна false. Однокурсник сказал: «Для отладки». Думаю: что тут можно отлаживать? Но не всё так просто.

Запускаю. Выполняется код в ветке default. Самое первое предположение — забыли поставить break. Внимательно перечитываю код — всё в порядке. Запускаю в отладчике и замечаю, что выполняется сразу ветка default, а в ветку true или false даже не заходит. Решаю просмотреть значение переменной. Visual Studio 2012 запущенная на Windows 7 показывает 205. Допустим. Плюсы я стал учить недавно, поэтому решил провести небольшой опыт: создал булевую переменную и присвоил ей 205. Отрабатывает ветка true. Пока я размышлял над этой проблемой, однокурсник нашёл решение этой проблемы: оказывается, массив не был инициализирован. После инициализации проблема исчезла. Теперь у меня возник возник вопрос: неужели проблема в switch? Ради проверки я решил провести следующий опыт.

#include <iostream>
using namespace std;

int main() {
    bool *t = new bool[1];
    switch (t[0])
    {
    case true:
        cout << "true\n";
        break;
    case false:
        cout << "false\n";
        break;
    default:
        cout << "superposition\n";
        break;
    }
    if(t[0] == true) {
        cout << "true\n";
    } else if(t[0] == false) {
        cout << "false\n";
    } else {
        cout << "superposition\n";
    }
    if(t[0]) {
        cout << "true\n";
    } else if(!t[0]) {
        cout << "false\n";
    } else {
        cout << "superposition\n";
    }
    delete[] t;
    return 0;
}

Вариант с switch или первый вариант с if позволяет определить не инициализированную булевую переменную. Второй вариант с if никогда не заходит в третий вариант.

Как создать такую переменную Шрёдингера? Создайте динамический или статический массив. Используйте его перед инициализацией. А также, самое вкусное, то, что вы можете скопировать это неопределённое значение, даже в скалярную переменную. Где это может пригодится — решать вам.

Интересно также то, что если перед проверкой вставить:

char a = t[0];
t[0] = a;
то переменная наконец-то станет true. А если написать
bool a = t[0];
t[0] = a;


то ничего не изменится.

Позднее я повторил опыт на Visual Studio 2013 с 5 пакетом обновления запущенная на Windows 7. Там отладчик говорит, что неинициализированная переменная равна true, но несмотря на это в блок true не заходит. Также она говорит, что ветка default не нужна, так как все возможные метки уже заданы. Но несмотря на это заходит в блок default.

В Linux данная особенность не проявляется.

Комментарии (49)


  1. ov7a
    28.09.2017 13:17
    +21

    "Undefined behavior" можете добавить в теги. Вообще странно, что его существование и разная обработка в зависимости от компилятора вас удивляет.
    http://en.cppreference.com/w/cpp/language/ub


  1. ProLimit
    28.09.2017 13:46
    +11

    Может быть, потому что switch() работает с целыми числами, а true и false заменяются typedef-ами на 1 и 0 при компиляции? В неинициализированной переменной может быть любой мусор. Условие if(!var) сработает только если значение 0 или false, а вот switch() уже нет, там нужно точное совпадение.


    1. tyomitch
      28.09.2017 13:54
      +1

      Условие if(!var) сработает только если значение 0 или false, а вот switch() уже нет, там нужно точное совпадение.

      Умный компилятор (например, GCC) пользуется знанием того, что bool-переменная может иметь значение либо true, либо false, и (в любой корректной программе) третьего не дано.


      1. ProLimit
        28.09.2017 14:10

        Может вы правы, у меня GCC не ругается и не предупреждает если записать
        bool var = 10;
        Возможно он сразу приводит ее к «true» дополнительной инструкцией. Но если в коде нет явной инициализации, то нет и приведения, и в ячейке хранится любое число. Неявное приведение к int в switch() видимо не настолько «умное» чтобы его преобразовать к true/false, а скорее всего лишние проверки не добавляются по соображениям производительности: в конце-концов, Undefined behaviour лежит на совести программиста, а не компилятора. Он может толкьо предупредить.


        1. ploop
          28.09.2017 14:20

          Возможно он сразу приводит ее к «true» дополнительной инструкцией
          «true» понятие относительное, смотря как использовать.
          Если if (var) то будет true и будет выполняться (if делает сравнение на 0)
          Если if (var == true) то будет false, ибо true обозначен как «1», а у нас в переменной 10


          1. tyomitch
            28.09.2017 14:22
            +1

            Нет, неправда. Проверьте сами и убедитесь.


          1. ProLimit
            28.09.2017 14:26
            +2

            Это если совсем старый C. Начиная с C99 bool это нативный тип и компилятор умеет к нему приводить, поэтому вашем примере if(var == true) проверка пройдет.


            1. ploop
              28.09.2017 14:32
              +1

              Точно, проверил. Преобразует к единице на этапе инициализации. То есть bool var = 10 сделает var равной единице.

              Вот ведь как в память врезалось…


              1. bogolt
                30.09.2017 08:12

                В какой-то старой вижуал студии помню так и работало: если bool b = 10, то потом в ней хранилось именно это значение.


        1. tyomitch
          28.09.2017 14:20
          +1

          Даже не дополнительной инструкцией приводит, а сразу же кладёт туда 1.


        1. master1312
          29.09.2017 12:13
          +1

          GCC для примера с if-ами вообще странный код генерирует. Во-первых, он выкидывает напрочь ветку default из switch и выводит false, потому что в первом case-е сравнивает булевскую переменную с 1, когда не совпадает (а там же мусор, так что не совпадает) — сразу переходит на cout<<«false\n»;. А в обоих ифах gcc тупо сравнивает булевскую переменную с 0 и довольный выводит true. Ассемблерный код для if(t[0]) и if(t[0]==true) абсолютно одинаковый:
          0x80486e8 <+0x0048> 80 3e 00 cmpb $0x0,(%esi)
          0x80486eb <+0x004b> 75 43 jne 0x8048730 <main()+144>
          Подозреваю, что MVC для switch-а честно сравнивает переменную сначала с 1, потом с 0 и в результате попадает в default.
          А вот при явном присвоении «bool var=10;» gcc еще при компиляции преобразует 10 в true (то есть в 1), что в отладке видно.


          1. ivan386
            29.09.2017 12:43

            В gcc с включенной оптимизацией компилируете?


            1. master1312
              29.09.2017 12:58

              Таки да, забыл выключить. Теперь для switch-а результат «superposition» и bool сравнивается как int, с 1 и 0, для if-ов по прежнему true. Да и ассемблерный код для них все равно одинаковый. В общем-то логично, что gcc if(t[0]) и if(t[0]==true) одинаково воспринимает, так же как и if(!t[0]) и if(t[0]==false).


    1. izzholtik
      28.09.2017 19:16
      +1

      это странно. Я думал, там сразу jz и jnz используются, без сравнения.


  1. ploop
    28.09.2017 13:57
    +2

    Замените bool на int и подставьте в условие вместо true "== 0" и вместо false "== 1" и всё станет на свои места.

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


    1. romy4
      28.09.2017 23:37

      bool по стандарту char


      1. ploop
        29.09.2017 00:19

        --


      1. ixSci
        29.09.2017 08:40
        +4

        bool, по стандарту, это bool.


        1. domix32
          29.09.2017 11:11

          Он имел ввиду размер переменной типа `bool`. То бишь в размер машинного слова и на дефолтных архитектурах может принимать значения от 0 до 255.


          1. ixSci
            29.09.2017 12:17

            А где в стандарте указано, что sizeof(char) == sizeof(bool)?


            1. tyomitch
              29.09.2017 14:48
              +1

              Там наоборот подчёркивается, что sizeof(bool) не стандартизован.


            1. domix32
              29.09.2017 22:42

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


              1. ploop
                30.09.2017 00:19

                Однако, слово может быть 32/64 бита (внутри). А bool в пакованных структурах компилятор вроде как приводит к char. В не пакованных хз, да и надо ли оно вникать…


                1. sumanai
                  30.09.2017 01:39
                  +2

                  В не пакованных хз, да и надо ли оно вникать…

                  Там ничего сложного, и сто раз обсасывалось, в том числе и на Хабре.


    1. ivan386
      29.09.2017 12:39

      подставьте в условие вместо true "== 0" и вместо false "== 1"

      Разве не наоборот? Вместо true "== 1" и вместо false "== 0".


      1. ploop
        29.09.2017 19:43

        Наоборот естественно… спешил.


  1. rkfg
    28.09.2017 14:43
    +6

    С UB бывают всякие «весёлые» эффекты. Например, null reference может быть одновременно == 0 и != 0, может быть равна другой такой же ссылке или не равна, в зависимости от версии компилятора и флагов оптимизации: пришлось рефакторить всю систему обработки ошибок. Простое правило: если вы полагаетесь на UB, вы добавляете в код немного святого рандома. Каждый компилятор вправе превратить этот код во что угодно, и он превратит. Garbage in => garbage out.


  1. hdfan2
    28.09.2017 16:29

    Хм, странно, что на Линуксе нет. Именно на Линуксе на GCC ловил похожий баг: неинициализированная переменная типа bool, при приведении к int получалось не 0/1, а то значение байта, что лежало в bool. На Windows отрабатывало правильно.


  1. romy4
    28.09.2017 16:30
    +14

    ответ на вопрос почему — в стандарте
    standard
    Values of type bool are either true or false(48)
    [Note:There are no signed,unsigned,short, or long bool types or values.— end note]
    Values of type bool participate in integral promotions (4.5)

    48) Using a bool value in ways described by this International Standard as “undefined,” such as by examining the value of an
    uninitialized automatic object, might cause it to behave as if it is neither true nor false.


    1. nolane
      28.09.2017 20:27
      +3

      Хоть один комментарий по делу.


      1. laughman
        29.09.2017 09:02

        Вообще-то, нет ничего хорошего в том, что bool может иметь третье значение, не true и не false. Это противоречит основному пониманию логических переменных, общепринятому в мире (спросите у любого не знающего С++, но знакомого с логическими операциями, И, ИЛИ, сколько значений может иметь логическая переменная), а язык заявляет реализацию ООП, который исходит из объектов и свойств реальных объектов мира. И во всех книжках по С++, что я читал, (и уверен в абсолютном большинстве книг ро С++), тип bool представлен как имеющий два возможных значения.
        Это приводит к тому, что для нормальной работы на языке мало прочесть книгу по С++, мало к тому прочесть еще и Саттера, Мейерса, но надо еще и стандарт знать. Причем, это все совершенно неочевидно после прочтения книг, которые пишутся как раз для того, чтобы можно было после них нормально писать код.
        Ну а то, что над этим «превосходством» над обычной логикой, где у логической переменной два значения, и планирование логических операций строится в рамках этого диапазона, будут смеяться пишущие на других языках — это лишь мелкий побочный эффект.


        1. romy4
          29.09.2017 09:08
          +2

          А много ли людей пишут код, делая switch для булевой переменной вместо одного if?

          if ( a ) 
          {
          }
          else
          {
          }


        1. ixSci
          29.09.2017 09:11
          +2

          Всё верно, bool-переменная может иметь только два значения: true, false. Никакого третьего значения там быть не может. Undefined behavior, по определению, означает, что программа может вести себя как угодно, и никакую логику к ней применить уже нельзя.


          1. d-stream
            30.09.2017 00:18
            -1

            В SQL бывает третье значение — NULL и зачастую это бывает полезно.


            1. tyomitch
              30.09.2017 17:41

              Четырёхзначные логики тоже применяются.


        1. ProLimit
          29.09.2017 10:05
          +1

          А по моему это нормально. С++ остается высокопроизводительным языком, и нагружать его лишними проверками не стоит. Если нужны все проверки и защита от дурака, то есть более высокоуровневые языки. А тут остаются предупреждения компилятора, и если уж программист использует switch() для булевой переменной и игнорирует предупреждения — сам виноват.


        1. Cryvage
          02.10.2017 17:23
          +1

          нет ничего хорошего в том, что bool может иметь третье значение, не true и не false.

          Это только в случае неопределённого поведения (undefined behavior). Но в случае неопределённого поведения, по стандарту, может происходить вообще что угодно, на усмотрение компилятора. Потому оно и неопределённое. Так что здесь нечему удивляться.
          Это приводит к тому, что для нормальной работы на языке мало прочесть книгу по С++, мало к тому прочесть еще и Саттера, Мейерса, но надо еще и стандарт знать. Причем, это все совершенно неочевидно после прочтения книг, которые пишутся как раз для того, чтобы можно было после них нормально писать код.

          В любой нормальной книжке по C++ должна обязательно быть написана одна простая мысль: «Избегайте неопределённого поведения». Если в вашей книжке этого не сказано, то я даже не знаю. Наверное стоит взять другую книгу.


  1. rprokop
    28.09.2017 20:51
    +1

    В отладочной версии, свежевыделенный кусок кучи заливается неким магическим значением (0xCD), в целях отладки. Это фича Visual C++, причем отладочной конфигурации.
    www.microsoft.com/msj/1198/c/c1198.aspx

    Выделенная в стеке память в отладочной конфигурации заливается 0xCC.


    1. tyomitch
      28.09.2017 22:54
      -1

      Да, но удивление вызывает не это, а то, что MSVC генерирует код для случая "bool-значение не равно ни true, ни false" — случая, запрещённого стандартом.
      Для сравнения, умный компилятор вообще удаляет из этого кода ветку default, потому что знает, что стандарт гарантирует, что эта ветка не понадобится.


    1. hdfan2
      29.09.2017 08:06

      Таких значений в Visual Studio намного больше.


  1. izzholtik
    29.09.2017 00:05

    У кого есть VS, прогоните плз

    Заголовок спойлера
    int main(int argc, char** argv) {
    
        bool data;
        
        memset((void*)&data, 42, 1);
        
        switch (data)
        {
            case true:
            {
                cout << "true";
                break;
            }
            case false:
            {
                cout << "false";
                break;
            }
            default:
            {
                cout << "default";
                break;
            }
        }
    }


    1. tyomitch
      29.09.2017 01:49

      Под clang так же; а под MSVC — сравнивает с единицей.
      (Причём и clang, и GCC удаляют обе недостижимые ветви прямо на этапе компиляции.)


      1. izzholtik
        29.09.2017 01:50

        а почему именно последний бит?


        1. tyomitch
          29.09.2017 10:36

          Стандарт гарантирует, что проверки младшего бита достаточно. (Как и сравнения с единицей достаточно.)
          Выбор из многих возможных способов отличить ноль от единицы — личное дело разработчиков компилятора. Наверное, на каких-то процессорах эффективнее одно, на каких-то — другое.


          1. nolane
            02.10.2017 21:46

            Стандарт гарантирует

            Сильное заявление. А пруфы можно?


            1. tyomitch
              02.10.2017 23:54

              Чуть выше в комментариях уже процитировали: «Values of type bool are either true or false»


              1. nolane
                03.10.2017 01:16
                +1

                Пожалуй так, но только не потому что они "true or false", а потому что
                "Values of type bool participate in integral promotions (4.5)":
                4.5.6 A prvalue of type bool can be converted to a prvalue of type int, with false becoming zero and true becoming one.
                Вы же о побитовых операциях говорите и сравнении с целым.


  1. Jef239
    29.09.2017 01:09
    -1

    Второй вариант с if никогда не заходит в третий вариант.

    А можно перевести на русский? А то получается примерно так:

    Чтобы все наши устройства были в порядке,я в срочном порядке был призван к порядку, чтобы обрабатывать данные разных порядков на несколько порядков быстрее.


  1. mike_y_k
    29.09.2017 16:04
    -2

    Спасибо за статью.
    Наконец найден ответ на извечный вопрос о качестве продукции Microsoft.
    Все вопросы теперь к разработчикам компилятора ;).