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++ ;) 

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


  1. RH215
    28.08.2019 14:24
    +18

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


    1. Alexey_Alive
      28.08.2019 14:36
      +10

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


      1. RH215
        28.08.2019 14:41
        +7

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

        А D язык хороший, но «не взлетел», к сожалению.


      1. yaroslavche
        28.08.2019 20:45
        +3


      1. aaprelev
        29.08.2019 08:05
        +2

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


        1. Alexey_Alive
          29.08.2019 14:34
          +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>


          1. aaprelev
            30.08.2019 09:57

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


    1. KanuTaH
      28.08.2019 14:37
      +6

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


      1. Sazonov
        28.08.2019 21:43
        +1

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


        1. KanuTaH
          28.08.2019 21:49
          +2

          Ну, для этого я и знакомлюсь с новыми стандартами, чтобы худо-бедно разбираться в «лямбдах с вариадиками» :) Так-то в принципе я и сам использую те же лямбды, constexpr, кортежи, считаю это удобными механизмами. Другое дело что всякие SFINAE кунштюки например я использую редко, это скорее для разработчиков библиотек, они, так сказать, страдают за нас, чтобы нам было проще и естественней использовать их библиотечные интерфейсы :)


    1. dipsy
      28.08.2019 14:46
      +6

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


      1. KanuTaH
        28.08.2019 14:48
        +1

        У Эллочки Людоедки был такой. Только названия у него нет — сэкономила одно слово в своем словаре.


      1. DoctorRoza
        28.08.2019 16:38
        +2

        Ну тогда Lisp, как Дядька Боб глаголит


      1. 0xd34df00d
        28.08.2019 16:58

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


      1. Whuthering
        28.08.2019 17:00
        -1

        Неужели Go?


      1. mapron
        28.08.2019 18:50
        +1

        Вариантов выше много — неужто Pascal?)


      1. NBAH79
        28.08.2019 19:45
        +1

        разбить программу на два слоя: оптимизированный на с++, и логику на шарпе


      1. alan008
        29.08.2019 08:03

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


      1. IGR2014
        29.08.2019 14:50

        Assembler?


    1. PeterK
      28.08.2019 16:52

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


      1. 0xd34df00d
        28.08.2019 16:59
        +9

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

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


        1. PeterK
          28.08.2019 18:04
          -1

          Неясно, когда его создавать/удалять. С созданием еще можно разобраться, а с удалением — труба…


          1. KanuTaH
            28.08.2019 18:12
            +9

            А? Классика же:

            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++, которые «ни за что и никогда», просто плохо знакомы с языком.


            1. PeterK
              28.08.2019 20:24
              +2

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


              1. KanuTaH
                28.08.2019 20:29

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


            1. AllexIn
              29.08.2019 08:36

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


              1. KanuTaH
                29.08.2019 10:02
                +3

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


                1. AllexIn
                  29.08.2019 10:05

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

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


                  1. KanuTaH
                    29.08.2019 10:26
                    +3

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


                  1. qw1
                    29.08.2019 10:46

                    deprecated и досвидания в следующей редакции
                    Верный путь убить язык, потому что если какой-то код будет компиляться с /std: с++14, но не /std:c++17 — будет расти фрагментация.


                    1. AllexIn
                      29.08.2019 10:49

                      Значит более жестко подходить на этапе перехода от экспериментальной ветки в стабильную.
                      Чтобы не было ситуаций, когда «Так, сегодня мы утверждаем переход в релиз фичи Х. Кстати, кто уже делал проекты с её использованием?.. Кто хотя бы пробовал?.. Ясно, переносим обсуждение на следующую встречу».
                      Напомню, примерно так было с одной из фич на последней встрече.

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


                      1. qw1
                        29.08.2019 13:00

                        Тут я согласен: новое нужно вводить с большой осторожностью. Но отзывать уже принятое — нельзя.

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


                  1. Antervis
                    29.08.2019 11:44
                    +1

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

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

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

                    какие фичи с++17, по-вашему, используют «два с половиной фаната»? С++14? С++20?


                    1. AllexIn
                      29.08.2019 12:07

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


                      1. Antervis
                        29.08.2019 12:22

                        Что там насчет «Garbage collector support»? :))

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

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

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


                        1. AllexIn
                          29.08.2019 12:33
                          -1

                          Ваше мнение же должно быть основано на конкретных примерах?

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


                  1. 0xd34df00d
                    29.08.2019 16:26
                    +3

                    Но вот тот же структурный биндинг из статьи — нафиг не нужен

                    Писать


                    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(...);

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


                    1. Ryppka
                      29.08.2019 18:29

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


                      1. 0xd34df00d
                        29.08.2019 18:39

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

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


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

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


                        1. Ryppka
                          29.08.2019 21:30

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

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


              1. Antervis
                29.08.2019 11:07

                Не удивлюсь если через пару лет в стандарте внезапно появится GC. Не, ну а чо, полезная же штука!

                у меня для вас новости из 11-ого года


                1. AllexIn
                  29.08.2019 11:11
                  +1

                  Счетчики ссылок(чем являются смартпоинтеры) и GC — сильно разные вещи.
                  Смартпоинтеры — логичное развитие голых указателей. По сути голые указатели не должны вообще хранится где-либо, кроме участка кода, который с ними непосредственно сейчас работает. Тут ни оверхеда по производительности нет, ни непредсказуемости.
                  А вот GC — совсем другая история. Но, повторюсь, все таки верю в светлое и надеюсь до имплементации GC в стандарте дело не дойдет.


                  1. Antervis
                    29.08.2019 11:39

                    я не про смартпоинтеры. По ссылке выше есть набор функций из категории «Garbage collector support», и он присутствует в стандарте языка с++. Другое дело что стандарт даже не требует от компилятора поддержки этого функционала чтобы считаться полностью соответствующим стандарту, разрабы компиляторов этот функционал не делают, а народу попросту пофиг.


                    1. AllexIn
                      29.08.2019 12:06

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


              1. 0xd34df00d
                29.08.2019 16:24
                +3

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

                Например?


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


      1. KanuTaH
        28.08.2019 17:49
        +5

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

        Разверните эту свою мысль. А то есть мнение, что вы до C++ так и не дошли, так сказать, перед тем, как от него уходить.


        1. qw1
          28.08.2019 18:19
          +1

          Возможно, речь о недавней статье habr.com/en/post/455848


          1. KanuTaH
            28.08.2019 18:32
            +2

            Ну не знаю, там речь все-таки не об утечке (деструктор-то у синглтона вызывается исправно, хехе), а о неопределенном поведении, вызванном конкретными «диверсионными» действиями со стороны пользователя библиотеки (понятно, что по незнанию). Случай интересный, но к «утечкам памяти» от синглтонов как таковых он все-таки отношения не имеет.


      1. mayorovp
        28.08.2019 19:37

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


        1. KanuTaH
          28.08.2019 19:38
          +1

          Видимо имеется в виду, что «если я создам синглтон через new, то некому будет вызвать delete». Что само по себе правильно, конечно, но синглтоны создаются не так.


          1. Whuthering
            28.08.2019 20:07

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


            1. KanuTaH
              28.08.2019 20:10

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


              1. Whuthering
                29.08.2019 10:16
                +1

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


  1. Ryppka
    28.08.2019 15:18
    +6

    Бывало в юные года…
    Писал на «сплюсплюс» тогда…
    Теперь угас уж жар в крови:
    На «Си» пишу, на «чистом С»…

    P.S.
    Статья хорошая, понравилась.


    1. WRP
      29.08.2019 11:12

      Вот и я о том же…
      Знаю С, но как понять синтаксис нового C++ для меня загадка.


      1. sborisov
        29.08.2019 16:20

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

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


  1. Raynk
    28.08.2019 20:45

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


    1. RH215
      28.08.2019 21:16
      +6

      C++ всё ещё удобнее в большинстве случаев. В С слишком много нужно делать руками.


      1. soniq
        28.08.2019 23:45

        Мне кажется, что вот эти трюки с явным-неявным управлением памятью требуют достаточно много когнитивных ресурсов. Когда пишешь на С — сосредоточен и внимателен ко всему, когда пишешь на С# — забиваешь на все эти детали. А С++ вроде сам за тебя много делает, но расслабляться нельзя.


        1. alex_justes Автор
          28.08.2019 23:51
          +1

          Сосредоточен и внимателен только до тех пор, пока код не становится достаточно большим и сложным, чтобы не помещаться целиком в когнитивные ресурсы...)


          1. soniq
            29.08.2019 00:08
            +1

            Перефразируя известное высказывание

            Программисты жалуются, что у них сложный код? Пусть простой пишут.


            1. alex_justes Автор
              29.08.2019 00:44
              +2

              Сам по себе код может быть простым, однако всё усложняется, когда проект становится большим: код пишет сразу целая команда, связи между модулями, библиотеками, опять же легаси (которое или уже было или появляется со временем)…

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

              Так что я предпочитаю использовать инструменты, которые как-то автоматизируют процесс, руками, конечно, хорошо, но, через некоторое время, это становится слишком «дорого».


              1. Ryppka
                29.08.2019 08:54

                Это общая проблема для всех языков: есть границы масштабирования при росте размера и сложности. В каждом языке есть свои средства для выражения абстракций, структурирования кода и т.д., но граница, за которой их начинает не хватать все равно есть. У C++, имхо, эта граница дальше, чем у C, но не сказать, что уж сильно дальше.
                Решить проблему можно только «внеязыковыми» средствами: архитектурой, продуманным дизайном и т.д. И вот тут есть интересный момент: можно добавлять в язык выразительные средства, а можно «опрощать» язык, чтобы уменьшать, так сказать, удельное логическое сопротивление на строчку кода. Получается, что кода-то очень много, но он «простой», в нем даже IDE может разобраться и построить схемы, навигацию и т.д. C — он изначально достаточно прост. А, к примеру, Java и Go сознательно сделаны такими.


        1. 0serg
          29.08.2019 09:25
          +2

          В C++ просто надо явно объяснять компилятору то, чего ты хочешь получить на выходе. Если понимать этот «метаязык» то проблем вообще никаких. Причем там же буквально несколько простых принципов, не сказать даже что что-то сложное. Особенно ярко это видно если сравнивать современный C++ с тем что было до него. Там где раньше было «правило трех / правило пяти» сейчас работает «правило нуля» — компилятор при правильном его использовании все сам соберет верно, ничего самостоятельно переопределять вручную не нужно.


          1. soniq
            29.08.2019 14:15
            -1

            У вас же есть CI? Посмотрите там в логах, сколько раз за последнюю неделю юнит-тесты плюсового кода падали с сегфолтом, а сколько раз по ассерту. Потом расскажете, какой компилятор умный, и как он не даёт ошибаться.


            1. 0serg
              29.08.2019 16:30
              +3

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


              1. 0xd34df00d
                29.08.2019 16:35

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

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


              1. soniq
                29.08.2019 18:28

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

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


                1. 0serg
                  29.08.2019 20:57

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

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


                  1. soniq
                    30.08.2019 00:02

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

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

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

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


                    1. 0serg
                      30.08.2019 00:11

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

                      Тогда Вы явно делаете что-то неправильно :)


                      1. soniq
                        30.08.2019 00:22

                        То есть, существуют некие очень простые правила, которые можно вставить, например, в плагин к IDE, и они бы подсветили этим товарищам место где они делают что-то явно неправильное и вот эти микро-факапы бы не случились?


                        1. 0serg
                          30.08.2019 16:07

                          Умные указатели + грамотно спроектированное приложение. К сожалению не скажу что это тривиально автоматизируется, но научиться вполне можно :)


                          1. soniq
                            30.08.2019 17:29

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


                            1. 0serg
                              30.08.2019 17:42

                              Корректность полностью проверить не может ни один язык кроме, возможно, функциональных :)


                              1. soniq
                                30.08.2019 19:33

                                Ну простые вещи уже много где научились проверять, не без влияния функциональщиков конечно, но что поделаешь. Чтобы получить в 2019 году NullReferenceException, или тем более IndexOutOfRangeException надо специальный комментарий рядом написать, иначе кодоанализатор загнобит.


        1. RH215
          29.08.2019 14:45

          Особенно нельзя расслабляться, если в используемых тобой библиотеках управляют памятью вручную. :)


    1. daiver19
      29.08.2019 08:36
      +1

      Какие современные языки умеют RVO, например? Мне кажется, что философия С++ достаточно самобытна, чтоб говорить не о натягивании совы на глобус, а просто о планомерном развитии. C++11 пофиксил многие проблемы тяжелого наследия С (привет, ручное управление памятью), теперь просто добавляют всякий сахар/оптимизации, как и другие современные языки.


      1. AllexIn
        29.08.2019 08:39
        -2

        Добавляют новые способы реализовать то, что и так уже можно нормально(без извращений) реализовать. Это усложнение языка и ведет только к плохому.


        1. DaylightIsBurning
          29.08.2019 15:07

          например, что такого добавили, что можно было без извращений реализовывать раньше?


          1. AllexIn
            29.08.2019 15:11
            -1

            Знаете что такое boost?
            Он весь реализован на базовом С++.
            И он почти весь переехал в стандарт.
            Кстати, далеко не самое плохое что внесли в стандарт.


            1. alex_justes Автор
              29.08.2019 15:45
              +2

              А вы заглядывали внутрь boost? Боюсь, если разобраться, как там внутри всё работает, то фильмы ужасов или самое извращенное порно могут начать казаться уже и не такими страшными…


            1. 0xd34df00d
              29.08.2019 16:34
              +3

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


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


            1. DaylightIsBurning
              29.08.2019 16:52
              +1

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

              Так что вопрос открыт: что такого добавили, что можно было без извращений реализовывать раньше?


      1. Antervis
        29.08.2019 11:33

        rust должен уметь — бекенд там llvm-ный, а RVO (насколько я в курсе) корректен для любого типа. Разве что если фронтенд такую оптимизацию прокидывать не умеет


    1. 0serg
      29.08.2019 09:20
      +4

      С++ остается идеальным языком для CAD-систем, к примеру. Или игр. Вообще для любых приложений сколь-либо крупного размера где важно быстродействие. И он очень удобен если его правильно уметь использовать. Емкий, выразительный и быстрый код. Взять тот же Eigen к примеру. Он очень C++-style, в любом другом языке по-моему его аналоги просто невозможны. Он дает понятный читаемый код. И при этом он (в моих тестах) весьма заметно опережал «c-style» MKL, который был еще и менее читаемым. Хотя вот казалось бы.

      Я резко не соглашусь с тем что C++ не является «современным языком». C++11 был радикальным шагом вперед, язык стал намного удобнее в использовании, более читабельным, более производительным. Ушла необходимость бороться с языком во многих местах. И сейчас C++17 — тоже большой шаг вперед. Не такой драматичный как C++11, но очень заметно упрощающий жизнь и убирающий потребность в некоторых критичные велосипедах.


  1. mikeus
    28.08.2019 23:24
    +2

    В примерах начиная с

    Base foo3(bool c)
    {
        Derived a,b;	
        if (c) {
            return std::move(a);
        }
        return std::move(b);
    }
    (если здесь предполагается что Derived это производный класс от Base) происходит object slicing. Заботиться при этом о реализации move semantics как-то уже излишне.


    1. KanuTaH
      28.08.2019 23:30

      В самом по себе object slicing нет ничего «незаконного», это просто механизм, у которого есть вполне легитимные применения. Почему бы и не озаботиться move semantics, если нужно?


    1. alex_justes Автор
      28.08.2019 23:43

      Как уже правильно заметили — это вполне себе рабочая практика. К тому же move — это, по сути, просто способ передать владение каким либо ресурсом без особых накладных расходов, и таким ресурсом как раз может быть что-нибудь общее, что как-то особенно считается в наследнике, например.


      1. 0serg
        29.08.2019 09:30

        По-моему Вы описываете очень экзотическую реализацию того что проще и лучше реализовывать паттерном Factory


        1. alex_justes Автор
          29.08.2019 15:49

          Фабрики бывают разными. Не скажу, что slicing — это хороший метод для реализаций чего-нибудь, но это используемый метод. Один из примеров вполне используемого кода (некоторая обёртка над сырыми указателями) есть в презентации из статьи (CppCon 2018: Arthur O'Dwyer “Return Value Optimization: Harder Than It Looks”)


          1. mikeus
            29.08.2019 22:29

            Просто срезка режет глаз. Но в контексте темы copy elision это наверно, да, может служить иллюстрацией, что вот так вот, можно вызвать move-конструктор от некоторой части объекта.


  1. WRP
    29.08.2019 11:08

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


    1. KanuTaH
      29.08.2019 11:17
      +1

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


      1. WRP
        29.08.2019 11:21

        Спасибо!
        Или упаду в пропасть или пойму.
        Вариантов два)


  1. MSerhiy
    29.08.2019 12:31
    +5

    Мне нравится как не с++ программисты обсуждают с++)


  1. WRP
    29.08.2019 17:51
    -2

    Вся вакханалия началась с STL, потом появился boost и прочее.
    То есть по сути библиотеки и шаблоны создали новый язык программирования.
    В итоге код на современном C++ ну явно нечеловеческий.
    Он нечитаем.
    100500 минусов в мою несуществующую карму, но это так.


    1. DaylightIsBurning
      29.08.2019 18:33

      Это всё появилось не от хорошей жизни, без него было ещё хуже.


      1. WRP
        29.08.2019 18:47

        Что значит «не от хорошей жизни»?


        1. sborisov
          29.08.2019 19:01

          Вот прекрасная статья от «отцов основателей», почему STL именно такая и какие задачи она решала.
          habr.com/ru/post/166849

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


  1. ss-nopol
    30.08.2019 12:13
    -3

    С++ уверенно движется по пути когда-то намеченным перлом. Если всё так продолжится то известную картинку можно будет переделывать под C++.

    image