Секреты тернарного оператора

    Каждый уважающий себя программист С\С++ знает что такое тернарный оператор и большинство использовало его хотя бы раз в своих программах. Но знаете ли вы все секреты тернарного оператора? Какие потенциальные опасности сопряжены с его использованием и какие, казалось бы не связанные с его прямым предназначением, возможности в нем таятся? Эта статья дает вам возможность проверить свои знания и, возможно, узнать что-то новое.
    Начнем с небольшого теста.

    Тест


    Скомпилируется ли следующий код? Объясните почему.
    1.
    int i;
    int j;
    (false ? i: j) = 45;
    

    2.
    int i;
    int j;
    (true ? i: j) = 45;
    

    3.
    short i;
    int j;
    (true ? i: j) = 45;
    

    4.
    return true ? 0 : 1;
    

    5.
    true ? return 0 : return 1;
    


    Какой будет вывод у следующего кусочка? Почему?
    6.
    std::cout << (false ? 9 : '9') << " " << (true ? 9 : '9');
    


    Какие значения будут у переменных a, b и c в результате выполнения следующего кода? Почему?
    7.
    int a = 1;
    int b = 1;
    int c = 1;
    a = true ? ++b : ++c;
    

    8. Назовите ситуацию, где нельзя использовать if{...} else{...}, но можно тернарный оператор.
    9. Какие потенциальные опасности скрываются в использовании тернарного оператора? В чем их причина?
    10. Какие неожиданные использования тернарного оператора приходят вам в голову?

    Объяснение


    Итак, начнем. Тернарный оператор выделяется из ряда других операторов в С++. Его называют "conditional expression". Ну а так как это expression, выражение, то как у каждого выражения, у него должен быть тип и value category. Собственно, ответив на вопросы какой тип и value category у тернарных операторов в каждом из первых семи вопросов теста, мы легко решим поставленные задачи.

    Здесь начинается самое интересное. Оказывается типом тернарного оператора будет наиболее общий тип его двух последних операндов. Что значит наиболее общий? Это легче всего пояснить на примерах. У int и short общим типом будет int.
    У A и B в следующем фрагменте общим типом будет также int.
    struct A{ operator int(){ return 1; } };
    struct B{ operator int(){ return 3; } };
    

    Т.е. наиболее общий тип это такой тип, к которому могу быть приведены оба операнда. Вполне могут быть ситуации, когда общего типа нет. Например у
    struct C{};
    struct D{};
    

    общего типа нет, и следующий фрагмент вообще не скомпилируется
    (true ? C() : D());
    

    Так. С типом тернарного оператора мы немного разобрались. Осталось решить вопрос с value category. Тут действует следующее правило: если в тернарном операторе происходит преобразование типов к наиболее общему, то тернарный оператор — rvalue. Если же нет, то lvalue. Теперь когда мы знаем то, что мы знаем, мы легко ответим на первые 7 вопросов.

    Ответы


    1. и 2. — Да. Преобразования типов не происходит, а lvalue вполне можно присваивать значение.
    3. — Нет. Здесь происходит преобразование типов. Значит value category у выражения слева от знака "=" — rvalue. А rvalue, как известно, нельзя присваивать.
    4. — Да. Все мы так делали не раз.
    5. — Нет. Здесь все дело в том, что в С++ statement не может разбивать expression.
    6. Программа выведет «57 9». В данном фрагменте из-за того, что 2ой и 3ий операнд имеют разные типы, происходит преобразование к наиболее общему типу. В данном случае int. А '9', как известно, имеет ASCII код 57.
    7. В этом вопросе кроется еще одна особенность тернарного оператора. А именно, вычисляется только тот операнд из второго и третьего, до которого доходит поток выполнения. Впрочем такое же поведение можно наблюдать у if{...}else{...}. Соответственно, значения переменных a, b и с будут 2, 2, 1.

    Где нельзя использовать if{...} else{...}, но можно тернарный оператор?


    Например, в списке инициализации конструктора. Вы не может написать так:
    struct S 
    {
    	S() : if(true) i_(1) else i_(0){}
    	int i_;
    };
    

    Но вполне можно вот так:
    struct S 
    {
    	S() : i_(some_condition ? 0 : 1){}
    	int i_;
    };
    


    При инициализации ссылки в зависимости от условия. Как известно, нельзя объявлять не инициализированную ссылку, поэтому следующий фрагмент не скомпилируется:
    int a = 3;
    int b = 4;
    int& i;
    if(some_condition)
      i = a;
    else
      i = b;
    

    А вот следующий скомпилируется успешно:
    int& i = (some_condition ? a : b);
    


    В С++11 тернарный оператор применяется гораздо чаще. Связано это с тем, что в constexpr функциях не должно быть ничего кроме return `expression`. А `expression` вполне может представлять из себя тернарный оператор.
    В качестве примера приведу классический алгоритм определения простоты числа

    constexpr bool check_if_prime_impl(unsigned int num, unsigned int d)
    {
      return (d * d > num) ? true : 
        (num % d == 0) ? false : 
          check_if_prime_impl(num, d + 1);
    }
     
    constexpr bool check_if_prime(unsigned int num)
    {
      return (num <= 1) ? false : 
        check_if_prime_impl(num, 2);
    }
    

    В этом же примере, кстати, видно использование каскадных тернарных операторов, которые могут быть неограниченной вложенности и заменять собой множественные if{...} else{...}.

    Опасности тернарного оператора


    Допустим у нас есть класс String
    class String
    {
      public:
      operator const char*();
    };
    

    И использовать мы его можем, например, так:
    const char* s = some_condition ? "abcd" : String("dcba");
    

    Как нам уже известно, второй и третий операнд тернарного оператора приводятся к наиболее общему типу. В данном случае это const char*. Но объект String(«dcba») уничтожится в конце выражения и s будет указывать на невалидную память. В лучшем случае программа упадет при попытке в дальнейшем использовать s. В худшем будет выдавать неверные результаты, вызывая недовольство у заказчика и головную боль у программиста.

    «Необычное» использование тернарного оператора


    Тернарный оператор можно использовать для определения общего типа двух и более типов. А это, в свою очередь, можно использовать, например, для определения приводится ли один тип к другому.
    template <typename T, typename U>
    struct common_type
    {
    	typedef decltype(true ? std::declval<T>() : std::declval<U>()) type;
    };
    
    template<typename T, typename U>
    struct is_same{ enum { value = false; } };
    
    template<typename T>
    struct is_same<T, T>{ enum { value = true; } };
    int main() 
    {
      std::cout << is_same<int, common_type<A, B>::type>::value <<std::endl;
    }
    

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

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 39

      +32
      На записи (condition? i: j ) = 45 я завис, потому что испугался, что не знаю Си. Оказывается, я не знаю C++, т.к. в обычном Си это все-таки не компилируется. Слава Богу, не помню, чтобы мне попадалось такое выражение (хотя саму конструкцию "?:" я люблю)
        +8
        Тот самый случай, когда лучше пару лишних IF. И овцы останутся целы.
          0
          В статье приведен тот случай, когда условие нужно выкинуть :)
          Будет ли оно компилироваться, если вместо true/false подставить булевую переменную? Под всеми платформами и компиляторами скомпилируется?
            –4
            Моя платформа Java, там подобная ересь вообще не скопмилируется :)
              0
              … написанная на с, cpp и прочей ереси ^_^
            +2
            Ну иногда тернарный оператор очень в тему; лично я часто использую его в printf'ах, когда нужно что-то по-разному печатать в зависимости от содержимого одного из аргументов (потому что очень не хочется писать условие, в котором printf в if и в else будут отличаться лишь мелочью). И, как мне кажется, это очень читаемо выглядит.
              +2
              Вроде такого:
              gtk_list_store_set(GTK_LIST_STORE(model), &iter, VM_TYPE,  strcmp((char *)type, "1") ? "ВМ" : "Шаблон", -1);
              

              не printf, но смысл тот же. Кажется, тернарный оператор для printf приводится как первый пример его использования в книге «Язык С» за авторством K & R. Там как раз множественные числа в тексте так обходились.
            +7
            В Си нет ссылок, поэтому общий тип, конечно, 'int', а не 'int&', и не работает. Но всё можно «исправить»:
            *(condition ? &i : &j) = 45;
            Не так красиво, но работает.
              +1
              Вот это хорошо!

              А то я тоже завис на таком извращении.
                +2
                Подсознательно как-то сразу понятнее, но OMG же )
                  0
                  А за такое в продакшене ТехДиректор по репе не настучит?
                    0
                    Этот топик не о продакшене, а о возможностях тернарного оператора. Я просто привёл «фикс» для примера в топике для Си. Но в принципе, иногда надо бывает выполнить что-то подобное, обычно делается так.
                    C++:
                    int& x = (condition ? i : j);
                    // some code
                    x = 45;
                    

                    C/C++:
                    int* x = (condition ? &i : &j);
                    // some code
                    *x = 45;
                    
                      0
                      Да, спорная запись. Заставлять компилятор размещать переменные в памяти, только ради того, чтобы не писать лишний if. И вдобавок, возможно, разрушить оптимизацию (изменив значение неизвестно какой переменной).
                      Хотя если подобное выражение находится в третьем выражении оператора for (что-нибудь вроде for(a=0,b=n;b-a>1;*(cond? &a: &b)=c){… } для бинарного поиска) — то туда if не вставить.
                  +15
                  В данном случае это const char*. Но объект String(«dcba») уничтожится в конце выражения и s будет указывать на невалидную память.


                  Там проблема не с тернарным оператором а с классом строки.

                  const char *s = String("Abcd");


                  будет падать ничуть не хуже.
                    0
                    упс
                      0
                      это аналогично такому

                          std::string a = "aaaaaaaaaaa";
                          const char* b =  a.c_str();
                          a.append("wwwwwwwwwwwwwwwwwwwww"); 
                      
                    +5
                    Спасибо за статью. То что тернарный оператор может работать как lvalue знал, а то что это поведение определяется общим типом — нет.

                    Я бы еще упомянул про опасности связанные с низким приоритетом тернарного оператора, которые с успехом были раскрыты в одном из постов PVS Studio. Ну и про то, что части оператора являются точками следования, тоже стоило сказать.
                      –3
                      По поводу четвертого пункта, но почему не «return !true;»? :-)
                      P.S. Интересно, почему я не могу пользоваться тегом?
                        +2
                        Может быть потому что статья про использование тернарного оператора и вместо пары 0 и 1 можно поставить 4 и 9?
                        –7
                        Часто пользуетесь в своем коде подобным?
                        Знать это хорошо, но знание ради знания это онанизм.
                          +9
                          Вы не рассказали о самой интересной и малоизвестной фиче тернарного оператора — возможности использовать throw в нем.
                            0
                            Приведите, пожалуйста, пример того, где это может быть оправдано.
                              +6
                              int foo() {
                                return valid ? some_state : throw std::logic_error();
                              }
                              


                              «Оправданность» в данном случае — понятие субъективное. Да, этот код можно написать с if. И он, наверное, будет так понятнее.
                                +2
                                Понятно, спасибо. Но с моей точки зрения это нехорошая практика. Выражение должно быть однозначное. Изначальный смысл тернарного оператора — выбор значения (тогда как условной конструкции — выбор ветви выполнения). Здесь же смешались в кучу кони, люди. Да, это компактнее, но стоит ли экономить на одном условии? Ради чего?

                                По своей сути это ничуть не лучше, чем выражение из недавней статьи про JavaScript:

                                !~utils.indexOf(adjacency, id) && adjacency.push(id);
                                

                                которое является способом записи условия:

                                if (utils.indexOf(adjacency, id) >= 0)
                                    adjacency.push(id);
                                
                                0
                                В constexpr функциях, чтобы можно было ловить различные ошибки в переданных аргументах на этапе компиляции. Подробнее можно посмотреть тут: scrutator.me/post/2013/11/19/constant_expressions.aspx
                            +10
                            Ну и заодно, при разговоре об общих типах, не вредно упомянуть о том, что void является допустимым типом операндов. Т.е. можно писать так:
                            true ? (void)C() : (void)D();
                            
                              +1
                              Багфикс:
                              class String
                              {
                                public:
                                const char* operator() (arguments-of-the-function-operator-must-go-here);
                                operator const char*() const; // оператор приведения типа
                              };
                              

                                0
                                Спасибо, поправил.
                                +1
                                class fake_logger
                                {
                                public:
                                    template<class T>
                                    void operator << (const T& any)
                                    {}
                                };
                                
                                #ifdef BUILD_ON_WINDOWS
                                    #define LOGGER() (true) ? __noop : fake_logger()
                                #else
                                    #define LOGGER() (true) ? void() : fake_logger()
                                #endif
                                

                                да макросы плохо, но бывает пользуюсь такой особеностью
                                чтобы «выключать» логгер в релизе и использовать синтаксис аля для стримов
                                LOGGER() << «blahblahblah»;

                                  0
                                  А это точно не будет генерировать код при отключенном логере?
                                    +1
                                    Я так понимаю, это равносильно
                                    if (false) fake_logger() << "blah-blah";
                                    
                                      0
                                      Точно? Не вижу я там «if (false)» Т.е. fake_logger дёргаться и параметры для "<<" него будут готовиться и если там строки, то это тяжёлые операции, которые будут выполнятся даже при отключённом логере.
                                        +2
                                        LOGGER() << "blahblahblah";
                                        

                                        Раскроется в:
                                        (true) ? void() : fake_logger() << "blahblahblah";
                                        

                                        что благодаря крайне высокому приоритету тернарного оператора эквивалентно:
                                        (true) ? void() : (fake_logger() << "blahblahblah");
                                        

                                        Так как до правой части дело не дойдёт, то и вычислений никаких там произведено не будет.
                                          +1
                                          Ох, спасибо. Что-то я протупил.
                                    +1
                                    Чем это лучше, чем
                                    define LOGGER()  fake_logger()
                                    
                                    ?
                                      +1
                                      в моем случае ещё бывает

                                      LOGGER() << formatted("....", param1, param2,...); 
                                      

                                      чтобы избежать вычисления параметров, хотелось бы чтобы этот код вообще не вызывался.
                                        +1
                                        Очень интересно. Хороший пример
                                    +1
                                    Спасибо за статью! Узнал много нового.

                                    Only users with full accounts can post comments. Log in, please.