«Скользкие» места C++17

    image

    В последние годы C++ шагает вперед семимильными шагами, и угнаться за всеми тонкостями и хитросплетениями языка бывает весьма и весьма непросто. Уже не за горами новый стандарт, однако внедрение свежих веяний — процесс не самый быстрый и простой, поэтому, пока есть немного времени перед C++20, предлагаю освежить в памяти или открыть для себя некоторые особо «скользкие» места актуального на данный момент стандарта языка. 

    Сегодня я расскажу: почему if constexpr не является заменой макросов, каковы «внутренности» работы структурного связывания (structured binding) и его «подводные» камни и правда ли, что теперь всегда работает copy elision и можно не задумываясь писать любой return. 

    Если не боишься немного «испачкать» руки, копаясь во «внутренностях» языка, добро пожаловать под кат.



    if constexpr


    Начнём, пожалуй, с самого простого — if constexpr позволяет еще на этапе компиляции отбросить ветку условного выражения, для которой желаемое условие не выполняется. 

    Кажется, что это замена макросу #if для выключения «лишней» логики? Нет. Совсем нет. 

    Во-первых, такой if обладает свойствами, недоступными для макросов, — внутри можно посчитать любое constexpr выражение, приводимое к bool. Ну а во-вторых, содержимое отбрасываемой ветки должно быть синтаксически и семантически корректным. 

    Из-за второго требования внутри if constexpr нельзя использовать, например, несуществующие функции (таким способом нельзя явно разделять платформо-зависимый код) или плохие с точки зрения языка конструкции (например « void T = 0;»).

    В чем же тогда смысл использования if constexpr? Основной смысл — в шаблонах. Для них есть специальное правило: отбрасываемая ветка не инстанцируется при инстанцировании шаблона. Это позволяет проще писать код, который каким-то образом зависит от свойств шаблонных типов.

    Однако и в шаблонах нельзя забывать о том, что код внутри веток должен быть корректным хотя бы для какого-нибудь (даже чисто потенциального) варианта инстанцирования, поэтому просто написать, например, static_assert(false) внутри одной из веток нельзя (нужно, чтобы этот static_assert зависел от какого-либо зависимого от шаблона параметра).

    Примеры:

    void foo()
    {
        // в обеих ветках ошибки, поэтому не скомпилируется
        if constexpr ( os == OS::win ) {
            win_api_call(); // под другими платформами будет ошибка
        }
        else {
            some_other_os_call(); // под win будет ошибка
        }
    }
    

    template<class T>
    void foo()
    {
        // Отбрасываемая ветка не инстанцируется, поэтому при правильном T код соберется
        if constexpr ( os == OS::win ) {
            T::win_api_call(); // если T поддерживает такой вызов, то ок под win
        }
        else {
            T::some_other_os_call(); // если T поддерживает такой вызов, то ок под другую платформу
        }
    }
    

    template<class T>
    void foo()
    {
        if constexpr (condition1) {
            // ...
        }
        else if constexpr (condition2) {
            // ...
        }
        else {
            // static_assert(false); // так нельзя
            static_assert(trait<T>::value); // можно, даже при том, что trait<T>::value всегда будет false
        }
    }
    

    О чём нужно помнить


    1. Код во всех ветках должен быть корректным. 
    2. Внутри шаблонов содержимое отбрасываемых веток не инстанцируется.
    3. Код внутри любой ветки должен быть корректным хотя бы для одного чисто потенциального варианта инстанцирования шаблона.

    Структурное связывание (structured binding)




    В C++17 появился достаточно удобный механизм декомпозиции различных кортежеподобных объектов, позволяющий удобно и лаконично привязывать их внутренние элементы к именованным переменным:

    // Самый частый пример использования — проход по ассоциативному массиву:
    for (const auto& [key, value] : map) {
        std::cout << key << ": " << value << std::endl;
    }
    

    Под кортежеподобным объектом я буду подразумевать такой объект, для которого известно количество доступных внутренних элементов на момент компиляции (от «кортеж» — упорядоченный список с фиксированным количеством элементов (вектор)).

    Под это определение попадают такие типы, как: std::pair, std::tuple, std::array, массивы вида «T a[N]», а также различные самописные структуры и классы.

    Стоп… В структурном связывании можно использовать свои собственные структуры? Спойлер: можно (правда, иногда придется поднапрячься (но об этом ниже)).

    Как оно работает


    Работа структурного связывания заслуживает отдельной статьи, но, раз мы говорим именно о «скользких» местах, я постараюсь кратко пояснить, как все устроено.

    В стандарте дается следующий синтаксис для определения связывания:

    attr(optional) cv-auto ref-operator(optional) [ identifier-list ] expression;

    • attr — опциональный список атрибутов;
    • cv-auto — auto с возможными модификаторами const/volatile;
    • ref-operator — опциональный спецификатор ссылочности (& или &&);
    • identifier-list — список имен новых переменных;
    • expression — выражение, дающее в результате кортежеподобный объект, который используется для связывания (expression может быть в виде «= expr», « {expr}» или «(expr)»).

    Важно отметить, что количество имен в identifier-list должно совпадать с количеством элементов в объекте, получаемом в результате выполнения expression.

    Это все позволяет писать конструкции вида:

    const volatile auto && [a,b,c] = Foo{};
    

    И тут мы попадем на первое «скользкое» место: встречая выражение вида «auto a = expr;», привычно подразумеваешь, что тип «a» будет вычислен по выражению «expr», и ожидаешь, что в выражении «const auto& [a,b,c] = expr;» будет сделано то же самое, только типы для «a,b,c» будут соответствующими const& типами элементов «expr»... 

    Истина же отличается: спецификатор «cv-auto ref-operator» используется для вычисления типа невидимой переменной, в которую присваивается результат вычисления expr (то есть компилятор заменяет «const auto& [a,b,c] = expr» на «const auto& e = expr»).

    Таким образом появляется новая невидимая сущность (здесь и далее буду называть ее {e} ), впрочем, сущность весьма полезная: например, она может материализовывать временные объекты (поэтому можно спокойно их связывать «const auto& [a,b,c] = Foo {};»). 

    Второе «скользкое» место вытекает сразу же из замены, которую делает компилятор: если тип, выведенный для {e}, не является ссылочным, то результат expr будет скопирован в {e}.

    Какие же типы будут у переменных в identifier-list? Начнем с того, что это будут не совсем переменные. Да, они ведут себя как самые настоящие, обычные переменные, но только с тем отличием, что внутри они ссылаются на связанную с ними сущность, причем decltype от такой «ссылочной» переменной будет выдавать тип именно сущности, на которую эта переменная ссылается:

    std::tuple<int, float> t(1, 2.f);
    auto& [a, b] = t; // decltype(a) — int, decltype(b) — float
    ++a; // изменяет, как «по ссылке», первый элемент t
    std::cout << std::get<0>(t); // выведет 2
    

    Сами же типы определяются следующим образом:

    1. Если {e} — массив (T a[N]), то тип будет один — T, cv-модификаторы будут совпадать с таковыми у массива. 
    2. Если {e} имеет тип E и поддерживает интерфейс кортежей — определены структуры:

      std::tuple_size<E>

      std::tuple_element<i, E>

      и функция:

      get<i>({e}); // или {e}.get<i>()

      то тип каждой переменной будет типом std::tuple_element_t<i, E>
    3. В иных случаях тип переменной будет соответствовать типу элемента структуры, к которой выполняется привязка. 

    Итак, если совсем кратко, при структурном связывании выполняются следующие шаги:

    1. Вычисление типа и инициализация невидимой сущности {e} исходя из типа expr и cv-ref модификаторов.
    2. Создание псевдопеременных и привязка их к элементам {e}.

    Структурное связывание своих классов/структур


    Главное препятствие к связыванию своих структур — отсутствие в C++ рефлексии. Даже компилятору, который, казалось бы, должен уж точно знать о том, как устроена внутри та или иная структура, приходится несладко: модификаторы доступа (public/private/protected) и наследование сильно затрудняют дело.

    Из-за подобных трудностей ограничения на использование своих классов весьма жесткие (по крайней мере пока: P1061, P1096):

    1. Все внутренние нестатические поля класса должны быть из одного базового класса, и они должны быть доступны на момент использования.
    2. Или класс должен реализовать «рефлексию» (поддержать интерфейс кортежей).

    // Примеры «простых» классов
    struct A { int a; }; 
    struct B : A {}; 
    struct C : A { int c; }; 
    class D { int d; };
    
    auto [a] = A{}; // работает (a -> A::a) 
    auto [a] = B{}; // работает (a -> B::A::a)
    auto [a, c] = C{}; // ошибка: a и c из разных классов
    auto [d] = D{}; // ошибка: d — private
    
    void D::foo()
    {
        auto [d] = *this; // работает (d доступен внутри класса)
    }
    

    Реализация интерфейса кортежей позволяет использовать любые свои классы для связывания, однако выглядит чуть громоздкой и таит в себе еще один «подводный камень». Давайте сразу на примере:

    // Небольшой класс, который должен возвращать ссылку на int при связывании
    
    class Foo;
    
    template<>
    struct std::tuple_size<Foo> : std::integral_constant<std::size_t, 1> {};
    
    template<>
    struct std::tuple_element<0, Foo>
    {
        using type = int&;
    };
    
    class Foo
    {
    public:
        template<std::size_t i>
        std::tuple_element_t<i, Foo> const& get() const;
    
        template<std::size_t i>
        std::tuple_element_t<i, Foo> & get();
    
    private:
        int _foo = 0;
        int& _bar = _foo;
    };
    
    template<>
    std::tuple_element_t<0, Foo> const& Foo::get<0>() const
    {
        return _bar;
    }
    
    template<>
    std::tuple_element_t<0, Foo> & Foo::get<0>()
    {
        return _bar;
    }
    

    Теперь «привязываем»:

    Foo foo;
    const auto& [f1] = foo;
    const auto  [f2] = foo;
    auto& [f3] = foo;
    auto  [f4] = foo;
    

    И самое время подумать, какие типы у нас получились? (Кто смог сразу ответить правильно, заслуживает вкусную конфетку.)

    decltype(f1);
    decltype(f2);
    decltype(f3);
    decltype(f4);
    

    Правильный ответ
    decltype(f1); // int&
    decltype(f2); // int&
    decltype(f3); // int&
    decltype(f4); // int&	
    ++f1; // это сработает и поменяет foo._foo, хотя {e} должен был быть const
    


    Почему так получилось? Ответ кроется в специализации по умолчанию для std::tuple_element:

    template<std::size_t i, class T>
    struct std::tuple_element<i, const T>
    {
        using type = std::add_const_t<std::tuple_element_t<i, T>>;
    };
    

    std::add_const не добавляет const к ссылочным типам, поэтому и тип для Foo будет всегда int&.

    Как это победить? Просто добавить специализацию для const Foo:

    template<>
    struct std::tuple_element<0, const Foo>
    {
        using type = const int&;
    };
    

    Тогда все типы будут ожидаемыми:

    decltype(f1); // const int&
    decltype(f2); // const int&
    decltype(f3); // int&
    decltype(f4); // int&
    ++f1; // это уже не сработает
    

    Кстати, это же поведение справедливо и для, например, std::tuple<T&>
    — можно получить неконстантную ссылку на внутренний элемент, даже несмотря на то, что сам объект будет константным. 

    О чем нужно помнить


    1. «cv-auto ref» в «cv-auto ref [a1..an] = expr» относится к невидимой переменной {e}.
    2. Если выведенный тип {e} не является ссылочным, {e} будет инициализирована копированием (осторожно с «тяжеловесными» классами).
    3. Связанные переменные — «неявные» ссылки (они ведут себя как ссылки, хотя decltype возвращает для них нессылочный тип (кроме тех случаев, когда переменная ссылается на ссылку)).
    4. Нужно быть внимательными при использовании ссылочных типов для связывания.

    Оптимизация возвращаемого значения (rvo, copy elision)




    Пожалуй, это была одна из самых бурно обсуждаемых фичей стандарта C++17 (по крайней мере, в моем кругу общения). И действительно: C++11 принес семантику перемещения, которая сильно упростила передачу «внутренностей» объекта и создание различных фабрик, а C++17 вообще, казалось бы, дал возможность не задумываться о том, как возвращать объект из какого-нибудь фабричного метода, — теперь все должно быть без копирования и вообще, «скоро и на Марсе все зацветет»…

    Но давайте будем немного реалистами: оптимизация возвращаемого значения — не самая простая для реализации штука. Очень рекомендую посмотреть вот это выступление с cppcon2018: Arthur O'Dwyer «Return Value Optimization: Harder Than It Looks», в котором автор рассказывает, почему это сложно. 

    Краткий спойлер:

    Есть такое понятие, как «слот для возвращаемого значения». Этот слот — по сути, просто место на стеке, которое выделяет тот, кто вызывает, и передает вызываемому. Если вызываемый код точно знает, какой единственный объект будет возвращен, он может просто сразу, напрямую создать его в этом слоте (при условии, что размер и тип объекта и слота совпадают).

    Что из этого следует? Давайте сразу разбирать на примерах.

    Здесь все будет хорошо — сработает NRVO, объект сконструируется сразу в «слоте»:

    Base foo1()
    {
        Base a;	
        return a;
    }
    

    Здесь уже нельзя однозначно определить, какой объект должен быть в итоге, поэтому будет неявно вызван move-конструктор (c++11):

    Base foo2(bool c)
    {
        Base a,b;	
        if (c) {	
            return a;	
        }
        return b;
    }
    

    Здесь чуточку сложнее… Так как тип возвращаемого значения отличается от объявленного типа, неявно move вызвать нельзя, поэтому по умолчанию вызовется copy-конструктор. Чтобы этого не произошло, нужно явно вызвать move:

    Base foo3(bool c)
    {
        Derived a,b;	
        if (c) {
            return std::move(a);
        }
        return std::move(b);
    }
    

    Казалось бы, это — то же самое, что и foo2, но тернарный оператор — весьма своеобразная штука

    Base foo4(bool c)
    {
        Base a, b;
        return std::move(c ? a : b);
    }
    

    Аналогично foo4, но еще и тип другой, поэтому move нужен точно:

    Base foo5(bool c)
    {
        Derived a, b;	
        return std::move(c ? a : b);
    }
    

    Как видно из примеров, над тем, как возвращать значение даже в, казалось бы, тривиальных случаях, все еще приходится задумываться… Есть ли способы немного упростить себе жизнь? Есть: clang с некоторых пор поддерживает диагностику необходимости явного вызова move, да и существует несколько предложений (P1155, P0527) в новый стандарт, которые сделают явный move менее нужным.

    О чем нужно помнить


    1. RVO/NRVO сработает только в том случае, если:
      • однозначно известно, какой единственный объект должен быть создан в «слоте возвращаемого значения»;
      • типы возвращаемого объекта и функции совпадают.
    2. Если есть неоднозначность в возвращаемом значении, то:
      • если типы возвращаемого объекта и функции совпадают — move будет вызван неявно;
      • иначе — надо явно вызвать move.
    3. Осторожно с тернарным оператором: он краток, но может потребовать явный move.
    4. Лучше использовать компиляторы с полезными диагностиками (или хотя бы статические анализаторы).

    Заключение


    И все-таки я люблю C++ ;) 
    Playrix
    75,29
    Разработчик мобильных free-to-play игр
    Поделиться публикацией

    Похожие публикации

    Комментарии 109

      +24
      В последнее время понял одну замечательную вещь: я стал уставать от C++. Слишком много неявных вещей, слишком много слов, слишком много борьбы с языком.
        +9

        Попробуй новые языки. Rust, например, предлагает безопасность намного выше, чем C++, не имеет тонны Легаси, а также не фанатичны к выборы парадигмы. D сейчас есть в варианте без сборщика мусора. А если нужно что-то простое, так ещё и "поближе" к железу, то есть старый добрый C с расширениями GCC. Там и автовывод типов добавили, и безопасные макросы, и дженерики, и даже RAII.

          +5
          Так как раз после Rust'а как раз C++ и воспринимается грустнее: много похожих на совеременный C++ идей, нет legacy, выглядит аккуратнее и без килотонн подводных камней.

          А D язык хороший, но «не взлетел», к сожалению.
          • НЛО прилетело и опубликовало эту надпись здесь
              +2

              Alexey_Alive, направьте меня в сторону автовывода типов? Не нашел среди C Extensions в документации GCC. RAII — это вы об атрибуте __cleanup__?

                +1

                Да, я про cleanup. Это некий аналог RAII, ибо в Си нет классов. По поводу вывода типов, в Gcc есть аналоги auto и decltype из cpp: __auto_type и typeof. Благодаря им можно писать безопасные макросы, например (в GCC в ({ }) последнее выражение возвращается.)


                #define max(a,b) \
                  ({ __auto_type _a = (a); \
                      __auto_type _b = (b); \
                    _a > _b ? _a : _b; }) <source>
                  0

                  Спасибо. Про __cleanup__ знал, про __auto_type — нет

              +8
              Ну не знаю, у меня нет какого-то ощущения «борьбы с языком». Появляются новые фичи, которые ты можешь использовать (предварительно разобравшись, как они работают), а можешь не использовать, это ж никто не заставляет.
                +3

                Поддерживаю. Но самое трудное — согласовать свои желания с коллегами. Чтобы не получилось ситуации, что один использует с++03, а другой фигачит лямбды с вариадиками.

                  +2
                  Ну, для этого я и знакомлюсь с новыми стандартами, чтобы худо-бедно разбираться в «лямбдах с вариадиками» :) Так-то в принципе я и сам использую те же лямбды, constexpr, кортежи, считаю это удобными механизмами. Другое дело что всякие SFINAE кунштюки например я использую редко, это скорее для разработчиков библиотек, они, так сказать, страдают за нас, чтобы нам было проще и естественней использовать их библиотечные интерфейсы :)
                +7
                Вот да, надо просто пользоваться языком, где всё явно, мало слов, нет особой борьбы с языком и можно кратко и выразительно оформить нужные алгоритмы в код, компилирующийся в быстрый исполняемый файл. Как там его, язык этот, не напомните название?
                  +1
                  У Эллочки Людоедки был такой. Только названия у него нет — сэкономила одно слово в своем словаре.
                    +2
                    Ну тогда Lisp, как Дядька Боб глаголит
                      0

                      Да это ж хаскель!

                        –2
                        Неужели Go?
                          +1
                          Вариантов выше много — неужто Pascal?)
                            +2
                            разбить программу на два слоя: оптимизированный на с++, и логику на шарпе
                              0

                              Delphi/Pascal конечно же

                                0
                                Assembler?
                                0
                                Я от С++ ушел около 3-х лет назад и ни разу не жалел. Статьи, подобные этой, укреплают меня в моем решении: никогда. В языке даже синглтон толком нельзя реализавать без утечки памяти. «Структурное связывание».
                                  +10
                                  В языке даже синглтон толком нельзя реализавать без утечки памяти.

                                  Это почему ещё?

                                    –2
                                    Неясно, когда его создавать/удалять. С созданием еще можно разобраться, а с удалением — труба…
                                      +11
                                      А? Классика же:

                                      class singleton
                                      {
                                      public:
                                          static singleton &instance()
                                          {
                                              static singleton inst;
                                      
                                              return inst;
                                         }
                                      private:
                                          singleton() {}
                                      
                                          singleton(const singleton &) = delete;
                                          singleton(singleton &&) noexcept = delete;
                                      
                                          singleton &operator=(const singleton &) = delete;
                                          singleton &operator=(singleton &&) noexcept = delete;
                                      };
                                      


                                      Что касается «с удалением — труба», так это тоже просто от плохого знания предмета:

                                      Destructors (12.4) for initialized objects of static storage duration (declared at block scope or at namespace scope) are called as a result of returning from main and as a result of calling exit (18.3). These objects are destroyed in the reverse order of the completion of their constructor or of the completion of their dynamic initialization. If an object is initialized statically, the object is destroyed in the same order as if the object was dynamically initialized. For an object of array or class type, all subobjects of that object are destroyed before any local object with static storage duration initialized during the construction of the sub- objects is destroyed.

                                      Я всегда замечал, что наиболее ярые критики C++, которые «ни за что и никогда», просто плохо знакомы с языком.
                                        +3
                                        Теперь возьмем конкретный пример, где используется синглетон почти универсально: логгер. И сколь-нибудь нетривиальную программу, где помимо локальных переменных/объектов на стеке всегда будет код, который выполняется после завершения main() и становится ясно, что этот логгер может уже и не существовать, когда он нам понадобится.
                                          0

                                          Ссылочку ниже прочли? :) Там и рецептик приводится. Понятно, что чуть сложнее, чем мой пример выше, и его тоже можно при очень сильном желании поломать, но тем не менее.

                                          0
                                          Конечно ярык критики С++ это те кто его не осилил.
                                          Так в этом и состоит суть критики: сложно осилить.
                                          Я каждый день пишу на С++ и только на С++.
                                          И я его сегодня знаю хуже чем лет пять назад.
                                          Работа комитета вызывает больше негатива, чем позитива.
                                          Да, язык нужно развивать. Но комитет ударился в впихивание всего подряд. Половину нового можно выкинуть, потому что оно является синтаксическим сахаром, мало нужным в повседневной работе. По сути просто пытаются один язык превратить в другой. На выходе получается гребаный франкенштейн. Не удивлюсь если через пару лет в стандарте внезапно появится GC. Не, ну а чо, полезная же штука!
                                            +4

                                            Ну, я не согласен. Например, spaceship operator — это синтаксический сахар? Да. Нужно ли выкинуть? ИМХО нет, потому что он избавляет от написания просто тонны boilerplate кода. Да, конечно, можно писать "по старинке", педалить все вот эти operator ==, !=, <, >, а ещё про friend operator не забыть, все по новой… Но зачем? В чем профит? А "ниасилить" тоже можно по-разному, можно не разбираться в тонкостях SFINAE, а можно быть неспособным реализовать синглтон без утечки памяти или испытывать сакральную боязнь object slicing'а, потому что где-то прочёл, что это "плохо", а почему именно это плохо и в каких случаях — не понимаешь, а ведь эти вещи ещё из C++98. В общем, незнание незнанию рознь.

                                              0
                                              Я не говорю, что всё что делает комитет хрень.
                                              Но вот тот же структурный биндинг из статьи — нафиг не нужен. Взять из переменной набор, вместо того чтобы обращаться к полям через переменную, что не составляет труда. К тому же тот же with из delphi гораздо более адекватное и красивое решение, если уж настолько критично не писать название переменной для доступа к члену…

                                              Вообще, ИМХО, комитету не хватает некой дополнительной «проверки временем». То есть добавили фичу, если через 5 лет этой фичей не пользуются большинство крупных игроков на С++ рынке — она выносится еще раз на обсуждение и если весомых доводов её оставить нет — depricated и досвидания в следующей редакции.
                                                +4

                                                Ну это я так понимаю скорее для всяких кортежей, особенно если в них ссылки. Вместо написания простынки из создания временных tuple и std::get.

                                                  0
                                                  deprecated и досвидания в следующей редакции
                                                  Верный путь убить язык, потому что если какой-то код будет компиляться с /std: с++14, но не /std:c++17 — будет расти фрагментация.
                                                    +1
                                                    Значит более жестко подходить на этапе перехода от экспериментальной ветки в стабильную.
                                                    Чтобы не было ситуаций, когда «Так, сегодня мы утверждаем переход в релиз фичи Х. Кстати, кто уже делал проекты с её использованием?.. Кто хотя бы пробовал?.. Ясно, переносим обсуждение на следующую встречу».
                                                    Напомню, примерно так было с одной из фич на последней встрече.

                                                    Засрать язык фичами, которые использует два с половиной фаната — это тоже верный способ убить язык.
                                                      0
                                                      Тут я согласен: новое нужно вводить с большой осторожностью. Но отзывать уже принятое — нельзя.

                                                      Нам, как пользователям стандарта, какое должно быть дело до обсуждаемого. Приняли — придётся с этим жить. Пока не приняли — даже и смотреть незачем, когда оно ещё будет принято…
                                                    +1
                                                    То есть добавили фичу, если через 5 лет этой фичей не пользуются большинство крупных игроков на С++ рынке — она выносится еще раз на обсуждение и если весомых доводов её оставить нет — depricated и досвидания в следующей редакции.

                                                    ну примерно так и происходит, только не 5 лет, а 2-3 года. Или вы никогда не пытались пользоваться бустом/experimental?

                                                    Засрать язык фичами, которые использует два с половиной фаната — это тоже верный способ убить язык.

                                                    какие фичи с++17, по-вашему, используют «два с половиной фаната»? С++14? С++20?
                                                      0
                                                      Что там насчет «Garbage collector support»? :))
                                                      А если серьезно вопрос достаточно сложный. Я на него не могу твердо ответить.
                                                      На любой мой ответ можно будет возразить: «Вот код, где это используется». Статистики то у меня нет, только ощущение от работы в разных командах плюсовиков.
                                                        0
                                                        Что там насчет «Garbage collector support»? :))

                                                        Это с++11, там да, есть несколько редко используемых фич. Собственно, после него в методологии развития стандарта многое поменялось.

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

                                                        ну вы назовите фичи, которые, на ваш взгляд, почти никто не использует. Ваше мнение же должно быть основано на конкретных примерах?
                                                          –1
                                                          Ваше мнение же должно быть основано на конкретных примерах?

                                                          Какое мнение? Я нигде не делал утверждение, что сейчас в стандарте есть фичи которые используется два калеки. Делать такое утверждение — большая ответственность. Надо быть большим экспертом. принимающим активное участие в анализе кодовой базы чтобы такое утверждение сделать.
                                                          Я сказал что делать такие фичи — ошибка. Обозначил тенденцию.
                                                      +5
                                                      Но вот тот же структурный биндинг из статьи — нафиг не нужен

                                                      Писать


                                                      for (const auto& [key, value] : someMap)
                                                      {
                                                          // ...
                                                      }

                                                      тупо удобнее, чем


                                                      for (const auto& pair : someMap)
                                                      {
                                                          const auto& key = pair.first;
                                                          const auto& value = pair.second;
                                                          // ...
                                                      }

                                                      Или там, не знаю,


                                                      const auto& [isNew, pos] = someMap.emplace(...);

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

                                                        0
                                                        Но, как и написано в статье, понимание того, что при этом будет создано, требует знаний и усилий. Не скажу, что критично, но осадочек остается: например, не будет ли значимых различий между компиляторами? Между версиями одного и того же компилятора?
                                                          0
                                                          Но, как и написано в статье, понимание того, что при этом будет создано, требует знаний и усилий.

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


                                                          Не скажу, что критично, но осадочек остается: например, не будет ли значимых различий между компиляторами? Между версиями одного и того же компилятора?

                                                          Нет, стандарт же вполне однозначно всё определяет.

                                                            0
                                                            Нет, стандарт же вполне однозначно всё определяет.

                                                            О да, стандарт сила, кто бы возражал. Вспомните, к примеру, так прекрасно разобранную О'Двайром историю про gcc, clang, msvc и разные форматы декорирования имен.
                                                            Что касается структурного связывания, то у меня не хватает ума, чтобы без просмотра сгенерированного ассемблера наверняка понять, что и как создается.
                                                    0
                                                    Не удивлюсь если через пару лет в стандарте внезапно появится GC. Не, ну а чо, полезная же штука!

                                                    у меня для вас новости из 11-ого года
                                                      +1
                                                      Счетчики ссылок(чем являются смартпоинтеры) и GC — сильно разные вещи.
                                                      Смартпоинтеры — логичное развитие голых указателей. По сути голые указатели не должны вообще хранится где-либо, кроме участка кода, который с ними непосредственно сейчас работает. Тут ни оверхеда по производительности нет, ни непредсказуемости.
                                                      А вот GC — совсем другая история. Но, повторюсь, все таки верю в светлое и надеюсь до имплементации GC в стандарте дело не дойдет.
                                                        0
                                                        я не про смартпоинтеры. По ссылке выше есть набор функций из категории «Garbage collector support», и он присутствует в стандарте языка с++. Другое дело что стандарт даже не требует от компилятора поддержки этого функционала чтобы считаться полностью соответствующим стандарту, разрабы компиляторов этот функционал не делают, а народу попросту пофиг.
                                                          0
                                                          Это сделано для того, чтобы те, кому нужен GC могли его относительно легко реализовать.
                                                      +3
                                                      Половину нового можно выкинуть, потому что оно является синтаксическим сахаром, мало нужным в повседневной работе.

                                                      Например?


                                                      Я в повседневной работе использую и folding expressions, и if constexpr, и structured bindings, и буду использовать кучу всякой ерунды из C++20 вроде того же уже упомянутого spaceship operator.

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

                                                Разверните эту свою мысль. А то есть мнение, что вы до C++ так и не дошли, так сказать, перед тем, как от него уходить.
                                                  +1
                                                  Возможно, речь о недавней статье habr.com/en/post/455848
                                                    +3
                                                    Ну не знаю, там речь все-таки не об утечке (деструктор-то у синглтона вызывается исправно, хехе), а о неопределенном поведении, вызванном конкретными «диверсионными» действиями со стороны пользователя библиотеки (понятно, что по незнанию). Случай интересный, но к «утечкам памяти» от синглтонов как таковых он все-таки отношения не имеет.
                                                      0
                                                      Не совсем исправно. В SingletonShared, пример 3 рассмотрен вариант, когда деструктор синглтона как раз не вызывается. Правда, это скорее не про утечку памяти, а про сбой требуемой деинициализации типа подчистки временных файлов.
                                                  0

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

                                                    +1
                                                    Видимо имеется в виду, что «если я создам синглтон через new, то некому будет вызвать delete». Что само по себе правильно, конечно, но синглтоны создаются не так.
                                                      0
                                                      Если у синглтона приватный конструктор, то его по-хорошему нельзя будет создать через new.
                                                        0

                                                        Можно, если создавать из статического метода этого же класса.

                                                          +1
                                                          Ну да, а можно еще и friend-классами обмазаться. Поэтому я и написал «по-хорошему», а если человек сам осознанно собирается отстрелить себе ногу, то понятное дело что C++ ему мешать в этом не будет :)
                                                +6
                                                Бывало в юные года…
                                                Писал на «сплюсплюс» тогда…
                                                Теперь угас уж жар в крови:
                                                На «Си» пишу, на «чистом С»…

                                                P.S.
                                                Статья хорошая, понравилась.
                                                  0
                                                  Вот и я о том же…
                                                  Знаю С, но как понять синтаксис нового C++ для меня загадка.
                                                    0
                                                    Си любят за то, что там таких подводных камней нет (есть другие наверное), к примеру — там всегда есть явное выделение памяти и удаление. То есть известно всегда какие инструкции будут выполнены, а не предположения о том какой конструктор будет вызван или не вызван, причём поведение это в С++ меняется от версии к версии (copy ellision).

                                                    Вот статья которая очень хорошо описывает подобную ситуацию с С++ (инициализация)
                                                    habr.com/ru/post/438492

                                                  0
                                                  У меня складывается ощущение, что из C++ пытаются сделать современный язык путем натягивания совы на глобус. Где нужна скорость, использую чистый C. Где надо быстро что-то сделать, использую C#. Кто для каких целей использует C++ с последними плюшками?
                                                    +7
                                                    C++ всё ещё удобнее в большинстве случаев. В С слишком много нужно делать руками.
                                                      0
                                                      Мне кажется, что вот эти трюки с явным-неявным управлением памятью требуют достаточно много когнитивных ресурсов. Когда пишешь на С — сосредоточен и внимателен ко всему, когда пишешь на С# — забиваешь на все эти детали. А С++ вроде сам за тебя много делает, но расслабляться нельзя.
                                                        +2
                                                        Сосредоточен и внимателен только до тех пор, пока код не становится достаточно большим и сложным, чтобы не помещаться целиком в когнитивные ресурсы...)
                                                          +1
                                                          Перефразируя известное высказывание
                                                          Программисты жалуются, что у них сложный код? Пусть простой пишут.
                                                            +3
                                                            Сам по себе код может быть простым, однако всё усложняется, когда проект становится большим: код пишет сразу целая команда, связи между модулями, библиотеками, опять же легаси (которое или уже было или появляется со временем)…

                                                            Да даже свой собственный код через некоторое время становится сложным для понимания/вспоминания.

                                                            Так что я предпочитаю использовать инструменты, которые как-то автоматизируют процесс, руками, конечно, хорошо, но, через некоторое время, это становится слишком «дорого».
                                                              0
                                                              Это общая проблема для всех языков: есть границы масштабирования при росте размера и сложности. В каждом языке есть свои средства для выражения абстракций, структурирования кода и т.д., но граница, за которой их начинает не хватать все равно есть. У C++, имхо, эта граница дальше, чем у C, но не сказать, что уж сильно дальше.
                                                              Решить проблему можно только «внеязыковыми» средствами: архитектурой, продуманным дизайном и т.д. И вот тут есть интересный момент: можно добавлять в язык выразительные средства, а можно «опрощать» язык, чтобы уменьшать, так сказать, удельное логическое сопротивление на строчку кода. Получается, что кода-то очень много, но он «простой», в нем даже IDE может разобраться и построить схемы, навигацию и т.д. C — он изначально достаточно прост. А, к примеру, Java и Go сознательно сделаны такими.
                                                          +3
                                                          В C++ просто надо явно объяснять компилятору то, чего ты хочешь получить на выходе. Если понимать этот «метаязык» то проблем вообще никаких. Причем там же буквально несколько простых принципов, не сказать даже что что-то сложное. Особенно ярко это видно если сравнивать современный C++ с тем что было до него. Там где раньше было «правило трех / правило пяти» сейчас работает «правило нуля» — компилятор при правильном его использовании все сам соберет верно, ничего самостоятельно переопределять вручную не нужно.
                                                            –2
                                                            У вас же есть CI? Посмотрите там в логах, сколько раз за последнюю неделю юнит-тесты плюсового кода падали с сегфолтом, а сколько раз по ассерту. Потом расскажете, какой компилятор умный, и как он не даёт ошибаться.
                                                              +4

                                                              Шутите? Это редчайшие события, как в тестах, так и в проде. В тестах хорошо если раз в год бывает сегфолт, в проде машин больше — там несколько случаев на сто тысяч запусков, но это в основном сбои железа, прежде всего памяти, они четко по машинам привязаны и при замене железа исчезают. Ассерты на логику да, могут вылетать но это с языком уже не связано.

                                                                0
                                                                Ассерты на логику да, могут вылетать но это с языком уже не связано.

                                                                Ну, на самом деле связано — чем больше проверок логики вы можете сделать в компилтайме, тем меньше ассертов у вас будет в рантайме (да и вообще тем меньше ассертов у вас будет).

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

                                                                  У нас тут распределённая сборка есть, так что можно посмотреть кто и что компилирует. На глаз, примерно 80% попыток вообще не компилируются. Я не поленился, и просмотрел последние 50 фейлов, когда проект собрался, юниты запустились но не прошли:
                                                                  В 41 случае сработал ассерт в тесте
                                                                  Ещё четыре падения когда сработал ассерт в продуктовом коде: попытались засунуть null в словарь, что-то не вызвали, и т.п.
                                                                  Ну и пять сегфолтов родимых.
                                                                    0
                                                                    Ну, эти 80% не компилирующегося кода — это как раз то что «компилятор не пропустил» :). Это можно назвать недостатком языка, а можно — достоинством (скажем python который я нежно люблю в той же ситуации запустится, но затем помрет в рантайме что хуже). Падающие тесты — тоже хорошо. Нередко это говорит о том что они отлавливают достаточно много нетривиальных ситуаций, поэтому их и не получается пройти «с первой попытки» (а то видел я тесты которые внешне выглядели похоже на правду но на деле не фейлились даже на ошибочном коде, хех :)). Не думаю кстати что с тестами ситуация будет отличаться в других языках.

                                                                    Но вот индексная арифметика — да, до коммита в прод может сегфолтить или ассертится, согласен. Разыменование null тоже бывает. Но это в любом языке будет ассертится, это ошибки в логике и не очень понятно при чем тут плюсы. А конкретно специфичную для плюсов работу с памятью в C++11 до сегфолта довести очень сложно.
                                                                      0
                                                                      Не анализировал, почему у них не собирается. Может в скобках запутались, или хедеров не хватило. А может быть действительно компилятор что-то обнаружил.

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

                                                                      А конкретно специфичную для плюсов работу с памятью в C++11 до сегфолта довести очень сложно.

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

                                                                        Тогда Вы явно делаете что-то неправильно :)
                                                                          0
                                                                          То есть, существуют некие очень простые правила, которые можно вставить, например, в плагин к IDE, и они бы подсветили этим товарищам место где они делают что-то явно неправильное и вот эти микро-факапы бы не случились?
                                                                            –1
                                                                            Умные указатели + грамотно спроектированное приложение. К сожалению не скажу что это тривиально автоматизируется, но научиться вполне можно :)
                                                                              0
                                                                              Получается, что компилятор С++ не может проверить корректность полностью, а значит за ним придётся доделывать нейронам в чьей-то голове. Автоматизация же, ну.
                                                                                +1
                                                                                Корректность полностью проверить не может ни один язык кроме, возможно, функциональных :)
                                                                                  0
                                                                                  Ну простые вещи уже много где научились проверять, не без влияния функциональщиков конечно, но что поделаешь. Чтобы получить в 2019 году NullReferenceException, или тем более IndexOutOfRangeException надо специальный комментарий рядом написать, иначе кодоанализатор загнобит.
                                                              0
                                                              Особенно нельзя расслабляться, если в используемых тобой библиотеках управляют памятью вручную. :)
                                                            +2
                                                            Какие современные языки умеют RVO, например? Мне кажется, что философия С++ достаточно самобытна, чтоб говорить не о натягивании совы на глобус, а просто о планомерном развитии. C++11 пофиксил многие проблемы тяжелого наследия С (привет, ручное управление памятью), теперь просто добавляют всякий сахар/оптимизации, как и другие современные языки.
                                                              –3
                                                              Добавляют новые способы реализовать то, что и так уже можно нормально(без извращений) реализовать. Это усложнение языка и ведет только к плохому.
                                                                0
                                                                например, что такого добавили, что можно было без извращений реализовывать раньше?
                                                                  0
                                                                  Знаете что такое boost?
                                                                  Он весь реализован на базовом С++.
                                                                  И он почти весь переехал в стандарт.
                                                                  Кстати, далеко не самое плохое что внесли в стандарт.
                                                                    +2
                                                                    А вы заглядывали внутрь boost? Боюсь, если разобраться, как там внутри всё работает, то фильмы ужасов или самое извращенное порно могут начать казаться уже и не такими страшными…
                                                                      +4

                                                                      Ну, во-первых, какого-нибудь фьюжона или ханы до сих пор нет.


                                                                      Во-вторых, у меня есть опыт портирования собственной библиотеки, довольно серьёзно обвешанной всяким метапрограммированием, с C++14 на C++17. И C++17, поверьте, очень сильно повышает удобство и качество жизни меня как реализатора библиотеки.

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

                                                                        Так что вопрос открыт: что такого добавили, что можно было без извращений реализовывать раньше?
                                                                    0
                                                                    rust должен уметь — бекенд там llvm-ный, а RVO (насколько я в курсе) корректен для любого типа. Разве что если фронтенд такую оптимизацию прокидывать не умеет
                                                                    +6
                                                                    С++ остается идеальным языком для CAD-систем, к примеру. Или игр. Вообще для любых приложений сколь-либо крупного размера где важно быстродействие. И он очень удобен если его правильно уметь использовать. Емкий, выразительный и быстрый код. Взять тот же Eigen к примеру. Он очень C++-style, в любом другом языке по-моему его аналоги просто невозможны. Он дает понятный читаемый код. И при этом он (в моих тестах) весьма заметно опережал «c-style» MKL, который был еще и менее читаемым. Хотя вот казалось бы.

                                                                    Я резко не соглашусь с тем что C++ не является «современным языком». C++11 был радикальным шагом вперед, язык стал намного удобнее в использовании, более читабельным, более производительным. Ушла необходимость бороться с языком во многих местах. И сейчас C++17 — тоже большой шаг вперед. Не такой драматичный как C++11, но очень заметно упрощающий жизнь и убирающий потребность в некоторых критичные велосипедах.
                                                                    +2
                                                                    В примерах начиная с
                                                                    Base foo3(bool c)
                                                                    {
                                                                        Derived a,b;	
                                                                        if (c) {
                                                                            return std::move(a);
                                                                        }
                                                                        return std::move(b);
                                                                    }
                                                                    (если здесь предполагается что Derived это производный класс от Base) происходит object slicing. Заботиться при этом о реализации move semantics как-то уже излишне.
                                                                      0
                                                                      В самом по себе object slicing нет ничего «незаконного», это просто механизм, у которого есть вполне легитимные применения. Почему бы и не озаботиться move semantics, если нужно?
                                                                        0
                                                                        Как уже правильно заметили — это вполне себе рабочая практика. К тому же move — это, по сути, просто способ передать владение каким либо ресурсом без особых накладных расходов, и таким ресурсом как раз может быть что-нибудь общее, что как-то особенно считается в наследнике, например.
                                                                          0
                                                                          По-моему Вы описываете очень экзотическую реализацию того что проще и лучше реализовывать паттерном Factory
                                                                            0
                                                                            Фабрики бывают разными. Не скажу, что slicing — это хороший метод для реализаций чего-нибудь, но это используемый метод. Один из примеров вполне используемого кода (некоторая обёртка над сырыми указателями) есть в презентации из статьи (CppCon 2018: Arthur O'Dwyer “Return Value Optimization: Harder Than It Looks”)
                                                                              0
                                                                              Просто срезка режет глаз. Но в контексте темы copy elision это наверно, да, может служить иллюстрацией, что вот так вот, можно вызвать move-конструктор от некоторой части объекта.
                                                                        0
                                                                        C++ новейший настолько сильно отличается от C и «C с классами», что просто дрожь берёт.
                                                                        Есть хоть какая-нибудь литература, чтобы попытаться сделать шаг через пропасть?

                                                                          +1

                                                                          Да, есть. Погуглите "C++ Core Guidelines".

                                                                            0
                                                                            Спасибо!
                                                                            Или упаду в пропасть или пойму.
                                                                            Вариантов два)
                                                                          +5
                                                                          Мне нравится как не с++ программисты обсуждают с++)
                                                                            –3
                                                                            Вся вакханалия началась с STL, потом появился boost и прочее.
                                                                            То есть по сути библиотеки и шаблоны создали новый язык программирования.
                                                                            В итоге код на современном C++ ну явно нечеловеческий.
                                                                            Он нечитаем.
                                                                            100500 минусов в мою несуществующую карму, но это так.
                                                                              +2
                                                                              Это всё появилось не от хорошей жизни, без него было ещё хуже.
                                                                                0
                                                                                Что значит «не от хорошей жизни»?
                                                                                  +1
                                                                                  Вот прекрасная статья от «отцов основателей», почему STL именно такая и какие задачи она решала.
                                                                                  habr.com/ru/post/166849

                                                                                  C++ успешен, т.к., вместо попытки предложить машинную модель, изобретенную разве что в процессе созерцания своего пупа, Бьярн начал с C и попытался развивать C далее, предоставляя больше техник обобщённого программирования, но в контексте рамок этой машинной модели. Машинная модель C очень проста. У вас есть память, где находятся сущности. У вас есть указатели на последовательные элементы памяти. Это очень просто для понимания. C++ сохраняет данную модель, но делает сущности, располагающиеся в памяти, более исчерпывающими, чем в машине C, т.к. C имеет ограниченный набор типов данных. А именно, C имеет структуры, предоставляющие разновидность расширяемой системы типов, но он не позволяет вам определять операции над структурами. Это ограничивает расширяемость системы типов. C++ существенно продвинул машинную модель C к действительно расширяемой системе типов.
                                                                              –3
                                                                              С++ уверенно движется по пути когда-то намеченным перлом. Если всё так продолжится то известную картинку можно будет переделывать под C++.

                                                                              image
                                                                                0
                                                                                Пора уже, мне кажется, разделять С++ на «С++ для разработчиков библиотек и компиляторов» и «С++ для обычных программистов». Я читаю все эти нюансы нюансов новых опциональных фич завтрашних стандартов — и офигеваю. Ума не приложу зачем мне это всё может понадобится. Я использую в ежедневном программировании крайне простое подмножество С++: классы, STL, ну там редкий шаблон пробежит раз в полгода. Я подозреваю, что если бы я писал, например, код какой-то библиотеки Boost, то мне понадобилось бы значительно больше инструментов. Но мне не нужно, а их всё пихают и пихают в язык.
                                                                                  +1
                                                                                  Вас разве кто-то заставляет все это использовать? :) А так например для меня из перечисленного как минимум structured binding выглядит полезным, так как я широко использую std::tuple и иже с ним. В «обычных программах», не в библиотеках.
                                                                                    0
                                                                                    Пока я пишу сам свой собственный проект, меня никто не заставит ничего использовать. Но когда над проектом работает команда (что, согласитесь, в среднем бывает значительно чаще), начинаются разночтения в предпочтениях. Вот тут то обширность инструментов и сыграет свою роль в формировании конфликтов мнений.
                                                                                      0
                                                                                      Это обычно как-то проводится через нормативные документы. Скажем, в гайдлайнах написано, что все исходные тексты должны гарантированно компилироваться с -std=c++98 (и соответственно настроен build server) и все — все пишут на «C с классами», без всех этих вот новомодных инструментов. А если в гайдлайнах написано, что использовать новые инструменты можно и нужно — ну да, придется использовать. Это скорее политический вопрос, чем технический.
                                                                                        0
                                                                                        Ну какой программист в своём уме перед командой заявит, что он не осиливает, поэтому давайте останемся на c++98 )))
                                                                                  +1
                                                                                  Хмм. А разве
                                                                                  return std::move(b);

                                                                                  не является бесполезным/вредным? Скотт Майерс в «Эффективный и современный С++. 42 рекомендации по использованию C++11 и C++14» писал, что в лучшем случае компилятор поймёт нас, хотя не обязан, а в худшем мы сломаем RVO/NRVO. Или в 17 стандарте что-то с тех пор кардинально поменялось?
                                                                                    0
                                                                                    Тут всё несколько сложнее:
                                                                                    компилятор, вероятно, попытается сделать всё, чтобы выбрать более оптимальный вариант вернуть значение, однако, если RVO/NRVO нельзя сделать или нельзя сделать неявный move, то значение будет копироваться (lvalue).

                                                                                    RVO/NRVO сломать очень легко, с неявным move есть как минимум defect report (CWG1579), который его запрещает, если тип функции и тип возвращаемого значения разные (даже если есть возможность сделать move), поэтому в некоторых случаях нужно явно писать move.

                                                                                    Можете посмотреть видео с cppcon, где есть чуть больше примеров: CppCon 2018: Arthur O'Dwyer “Return Value Optimization: Harder Than It Looks”

                                                                                    И поищите подробнее про диагностку у clang, например: -Wreturn-std-move

                                                                                    Рекомендации — это хороший вариант делать всё относительно хорошо в среднем (как минимум код будет работать), но бывает и так, что можно сделать более производительно/правильно, если разобраться, что за всем этим стоит на самом деле.

                                                                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                                                  Самое читаемое