C++17

    Рисунок 2


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

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


    Свертка параметров шаблона (Fold expressions)


    Для начала несколько слов о том, что вообще такое свертка списка (также известна как fold, reduce или accumulate).

    Свертка – это функция, которая применяет заданную комбинирующую функцию к последовательным парам элементов в списке и возвращает результат. Простейшим примером может служить суммирование элементов списка при помощи свертки:

    Пример из C++:

    std::vector<int> lst = { 1, 3, 5, 7 };
    int res = std::accumulate(lst.begin(), lst.end(), 0, 
      [](int a, int b)  { return a + b; });
    std::cout << res << '\n'; // 16

    Если комбинирующая функция применяется к первому элементу списка и результату рекурсивной обработки хвоста списка, то свертка называется правоассоциативной. В нашем примере получим:

    1 + (3 + (5 + (7 + 0)))

    Если комбинирующая функция применяется к результату рекурсивной обработки начала списка (весь список без последнего элемента) и последнему элементу, то свертка называется левоассоциативной. В нашем примере получим:

    (((0 + 1) + 3) + 5) + 7

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

    В C++17 появилась поддержка свертки для списка параметров шаблонов. Она имеет следующий синтаксис:
    (pack op ...) Унарная правоассоциативная свертка
    (… op pack) Унарная левоассоциативная свертка
    (pack op… op init) Бинарная правоассоциативная свертка
    (init op… op pack) Бинарная левоассоциативная свертка

    op – один из следующих бинарных операторов:

    + - * / % ^ & | ~ = < > << >> += -= *= /= %=
    ^= &= |= <<= >>= == != <= >= && || , .* ->*

    pack – выражение, содержащее нераскрытую группу параметров (parameter pack)

    init – начальное значение

    Вот, например, шаблонная функция, принимающая переменное число параметров и вычисляющая их сумму:

    // C++17
    #include <iostream>
    
    template<typename... Args>
    auto Sum(Args... args)
    {
      return (args + ...);
    }
    
    int main()
    {
      std::cout << Sum(1, 2, 3, 4, 5) << '\n'; // 15
      return 0;
    }

    Примечание: В данном примере функцию Sum можно было бы объявить как constexpr.

    Если мы хотим указать начальное значение, то используем бинарную свертку:

    // C++17
    #include <iostream>
    
    template<typename... Args>
    auto Func(Args... args)
    {
      return (args + ... + 100);
    }
    
    int main()
    {
      std::cout << Func(1, 2, 3, 4, 5) << '\n'; //115
      return 0;
    }

    До C++17 чтобы реализовать подобную функцию, пришлось бы явно указывать правила для рекурсии:

    // C++14
    #include <iostream>
    
    auto Sum()
    {
      return 0;
    }
    
    template<typename Arg, typename... Args>
    auto Sum(Arg first, Args... rest)
    {
      return first + Sum(rest...);
    }
    
    int main()
    {
      std::cout << Sum(1, 2, 3, 4); // 10
      return 0;
    }

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

    // C++17
    #include <iostream>
    
    template<typename T, typename... Args>
    void PushToVector(std::vector<T>& v, Args&&... args)
    {
      (v.push_back(std::forward<Args>(args)), ...);
    
      //Раскрывается в последовательность выражений через запятую вида:
      //v.push_back(std::forward<Args_1>(arg1)),
      //v.push_back(std::forward<Args_2>(arg2)),
      //....
    }
    
    int main()
    {
      std::vector<int> vct;
      PushToVector(vct, 1, 4, 5, 8);
      return 0;
    }

    Таким образом, свертка сильно упрощает работу с variadic templates.

    template<auto>


    Теперь в шаблонах можно писать auto для non-type template параметров. Например:

    // C++17
    template<auto n>
    void Func() { /* .... */ }
    
    int main()
    {
      Func<42>(); // выведет тип int
      Func<'c'>(); // выведет тип char
      return 0;
    }

    Ранее единственным способом передать non-type template параметр с неизвестным типом была передача двух параметров – типа и значения. Другими словами, ранее этот пример выглядел бы следующим образом:

    // C++14
    template<typename Type, Type n>
    void Func() { /* .... */ }
    
    int main()
    {
      Func<int, 42>();
      Func<char, 'c'>();
      return 0;
    }

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


    До C++17 вывод типов шаблонных параметров работал только для функций, из-за чего при конструировании шаблонного класса всегда было нужно в явном виде указывать шаблонные параметры:

    // C++14
    auto p = std::pair<int, char>(10, 'c');

    либо использовать специализированные функции вроде std::make_pair, для неявного вывода типов:

    // C++14
    auto p = std::make_pair(10, 'c');

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

    #include <tuple>
    #include <array>
    
    template<typename T, typename U>
    struct S
    {
      T m_first;
      U m_second;
      S(T first, U second) : m_first(first), m_second(second) {}
    };
    
    int main()
    {
      // C++14
      std::pair<char, int> p1 = { 'c', 42 };
      std::tuple<char, int, double> t1 = { 'c', 42, 3.14 };
      S<int, char> s1 = { 10, 'c' };
    
      // C++17
      std::pair p2 = { 'c', 42 };
      std::tuple t2 = { 'c', 42, 3.14 };
      S s2 = { 10, 'c' };
    
      return 0;
    }

    Стандартом было определено множество правил вывода типов (deduction guides). Также предоставляется возможность самим писать эти правила, например:

    // C++17
    #include <iostream>
    
    template<typename T, typename U>
    struct S
    {
      T m_first;
      U m_second;
    };
    
    // Мой deduction guide
    template<typename T, typename U>
    S(const T &first, const U &second) -> S<T, U>;
    
    int main()
    {
      S s = { 42, "hello" };
      std::cout << s.m_first << s.m_second << '\n';
    
      return 0;
    }

    Большинство стандартных контейнеров работают без необходимости вручную указывать deduction guide.

    Примечание: компилятор может вывести deduction guide автоматически из конструктора, но в данном примере у структуры S нет ни одного конструктора, поэтому и определяем deduction guide вручную.

    Таким образом, вывод типов для классов позволяет значительно сократить код и забыть о таких функциях как std::make_pair, std::make_tuple, и использовать вместо них конструктор.

    Constexpr if


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

    // C++17
    #include <iostream>
    #include <type_traits>
    
    template <typename T>
    auto GetValue(T t)
    {
      if constexpr (std::is_pointer<T>::value)
      {
        return *t;
      }
      else
      {
        return t;
      }
    }
    
    int main()
    {
      int v = 10;
      std::cout << GetValue(v) << '\n'; // 10
      std::cout << GetValue(&v) << '\n'; // 10
    
      return 0;
    }

    До C++17 нам пришлось бы использовать SFINAE и enable_if:

    // C++14
    template<typename T>
    typename std::enable_if<std::is_pointer<T>::value,
      std::remove_pointer_t<T>>::type
    GetValue(T t)
    {
      return *t;
    }
    
    template<typename T>
    typename std::enable_if<!std::is_pointer<T>::value, T>::type
    GetValue(T t)
    {
      return t;
    }
    int main()
    {
      int v = 10;
      std::cout << GetValue(v) << '\n'; // 10
      std::cout << GetValue(&v) << '\n'; // 10
    
      return 0;
    }

    Не трудно заметить, что код с constexpr if на порядок читабельнее.

    Constexpr лямбды


    До C++17 лямбды не были совместимы с constexpr. Теперь лямбды можно писать внутри constexpr выражений, а также можно объявлять сами лямбды как constexpr.

    Примечание: даже если спецификатор constexpr не указан, лямбда все равно будет constexpr, если это возможно.

    Пример с лямбдой внутри constexpr функции:

    // С++17
    constexpr int Func(int x)
    {
      auto f = [x]() { return x * x; };
      return x + f();
    }
    
    int main()
    {
      constexpr int v = Func(10);
      static_assert(v == 110);
    
      return 0;
    }

    Пример с constexpr лямбдой:

    // C++17
    int main()
    {
      constexpr auto squared = [](int x) { return x * x; };
      constexpr int s = squared(5);
      static_assert(s == 25);
    
      return 0;
    }

    Захват *this в лямбда-выражениях


    Теперь лямбда-выражения могут захватывать члены класса по значению при помощи *this:

    class SomeClass
    {
    public:
      int m_x = 0;
    
      void f() const
      {
        std::cout << m_x << '\n';
      }
    
      void g()
      {
        m_x++;
      }
    
      // С++14
      void Func()
      {
        // const копия *this
        auto lambda1 = [self = *this](){ self.f(); };
        // non-const копия *this
        auto lambda2 = [self = *this]() mutable { self.g(); };
        lambda1();
        lambda2();
      }
    
      // С++17
      void FuncNew()
      {
        // const копия *this
        auto lambda1 = [*this](){ f(); }; 
        // non-const копия *this
        auto lambda2 = [*this]() mutable { g(); };
        lambda1();
        lambda2();
      }
    };

    inline переменные


    В C++17 в дополнение к inline функциям появились также inline переменные. Переменная или функция, объявленная inline, может быть определена (обязательно одинаково) в нескольких единицах трансляции.

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

    (Вместо того, чтобы писать extern и присваивать значение в .cpp)

    header.h:

    #ifndef _HEADER_H
    #define _HEADER_H
    inline int MyVar = 42;
    #endif

    source1.h:

    #include "header.h"
    ....
    MyVar += 10;

    source2.h:

    #include "header.h"
    ....
    Func(MyVar);

    До C++17 пришлось бы объявлять переменную MyVar как extern и в одном из .cpp файлов присваивать ей значение.

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


    Появился удобный механизм для декомпозиции таких объектов, как, например, пары или кортежи, называемый Structured bindings или Decomposition declaration.

    Продемонстрирую его на примере:

    // C++17
    #include <set>
    
    int main()
    {
      std::set<int> mySet;
      auto[iter, ok] = mySet.insert(42);
      ....
      return 0;
    }

    Метод insert() возвращает pair<iterator, bool>, где iterator является итератором на вставленный объект и bool, который принимает значение false, если элемент не был вставлен (т.е. уже содержался в mySet).

    До C++17 нужно было бы использовать std::tie:

    // C++14
    #include <set>
    #include <tuple>
    
    int main()
    {
      std::set<int> mySet;
      std::set<int>::iterator iter;
      bool ok;
      std::tie(iter, ok) = mySet.insert(42);
      ....
      return 0;
    }

    Очевидным недостатком является то, что переменные iter и ok приходится объявлять заранее.

    Помимо этого, структурное связывание можно использовать с массивами:

    // C++17
    #include <iostream>
    
    int main()
    {
      int arr[] = { 1, 2, 3, 4 };
      auto[a, b, c, d] = arr;
      std::cout << a << b << c << d << '\n';
    
      return 0;
    }

    Можно также производить декомпозицию типов, содержащих только нестатические открытые члены.

    // C++17
    #include <iostream>
    
    struct S
    {
      char x{ 'c' };
      int y{ 42 };
      double z{ 3.14 };
    };
    
    int main()
    {
      S s;
      auto[a, b, c] = s;
      std::cout << a << ' ' << b << ' ' << c << ' ' << '\n';
    
      return 0;
    }

    На мой взгляд, очень удачным применением структурного связывания является его использование в range-based циклах:

    // C++17
    #include <iostream>
    #include <map>
    
    int main()
    {
      std::map<int, char> myMap;
      ....
    
      for (const auto &[key, value] : myMap)
      {
        std::cout << "key: " << key << ' ';
        std::cout << "value: " << value << '\n';
      }
    
      return 0;
    }

    Инициализатор в if и switch


    В C++17 появились операторы if и switch с инициализатором:

    if (init; condition)
    switch(init; condition)

    Пример использования:

    if (auto it = m.find(key); it != m.end())
    {
      ....
    }

    Они удачно смотрятся в связке с упомянутым выше структурным связыванием. Например:

    std::map<int, std::string> myMap;
    ....
    if (auto[it, ok] = myMap.insert({ 2, "hello" }); ok)
    {
      ....
    }

    __has_include


    Предикат препроцессора __has_include позволяет проверить, доступен ли заголовочный файл для подключения.

    Приведу пример использования прямо из предложения к стандарту (P0061R1). Здесь подключаем optional если он доступен:

    #if __has_include(<optional>)
      #include <optional>
      #define have_optional 1
    #elif __has_include(<experimental/optional>)
      #include <experimental/optional>
      #define have_optional 1
      #define experimental_optional 1
    #else
      #define have_optional 0
    #endif

    Новые атрибуты


    В дополнение к уже существующим стандартным атрибутам [[noreturn]], [[carries_dependency]] и [[deprecated]] в C++17 появились 3 новых атрибута:

    [[fallthrough]]

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

    Небольшой пример:

    // C++17
    switch (i)
    {
    case 10:
      f1();
      break;
    case 20:
      f2();
      break;
    case 30:
      f3();
      break;
    case 40:
      f4();
      [[fallthrough]]; // Предупреждение будет подавлено
    case 50:
      f5();
    }

    [[nodiscard]]

    Этот атрибут используется, чтобы обозначить, что возвращаемое значение функции должно быть обязательно использовано при вызове:

    // C++17
    [[nodiscard]] int Sum(int a, int b)
    {
      return a + b;
    }
    
    int main()
    {
      Sum(5, 6); // Будет выдано предупреждение компилятора/анализатора
      return 0;
    }

    Также [[nodiscard]] можно применять к типам данных или перечислениям, чтобы пометить все функции, возвращающие этот тип как [[nodiscard]]:

    // C++17
    struct [[nodiscard]] NoDiscardType
    {
      char a;
      int b;
    };
    
    NoDiscardType Func()
    {
      return {'a', 42};
    }
    
    int main()
    {
      Func(); // Будет выдано предупреждение компилятора/анализатора
      
      return 0;
    }

    [[maybe_unused]]

    Этот атрибут используется, чтобы подавить предупреждения компилятора/анализатора о неиспользуемой переменной, параметре функции, статической функции и прочем. Примеры:

    // Предупреждение будет подавлено
    [[maybe_unused]] static void SomeUnusedFunc() { .... }
    
    // Предупреждение будет подавлено
    void Foo([[maybe_unused]] int a) { .... }
    void Func()
    {
      // Предупреждение будет подавлено
      [[maybe_unused]] int someUnusedVar = 42;
      ....
    }

    Новый тип std::byte


    Тип std::byte предлагается использовать при работе с 'сырой' памятью. Обычно для этого используется char, unsigned char или uint8_t. Тип std::byte является более типобезопасным, так как к нему можно применить только побитовые операции, а арифметические операции и неявные преобразования недоступны. Другими словами, указатель на std::byte не удастся использовать в качестве фактического аргумента для вызова функции F(const unsigned char *).

    Этот новый тип определен в <cstddef>следующим образом:

    enum class byte : unsigned char {};

    Динамическое выделение памяти для типов с нестандартным выравниванием (Dynamic allocation of over-aligned types)


    В C++11 был добавлен спецификатор alignas, позволяющий вручную указать выравнивание для типа или переменой. До C++17 не было никаких гарантий того, что выравнивание будет выставлено в соответствии с alignas при динамическом выделении памяти. Теперь же стандарт гарантирует, что выравнивание будет учитываться:

    // C++17
    struct alignas(32) S
    {
      int a;
      char c;
    };
    
    int main()
    {
      S *objects = new S[10];
      ....
    
      return 0;
    }

    Более строгий порядок вычисления выражений


    В C++17 появились новые правила, более строго определяющие порядок вычисления выражений:
    • Постфиксные выражения вычисляются слева направо (в том числе вызовы функций и доступ к членам объектов)
    • Выражения присваивания вычисляются справа налево.
    • Операнды операторов << и >> вычисляются слева направо.

    Таким образом, как указывается в предложении к стандарту, в следующих выражениях теперь гарантированно сначала вычисляется a, затем b, затем c, затем d:

    a.b
    a->b
    a->*b
    a(b1, b2, b3)
    b @= a
    a[b]
    a << b << c
    a >> b >> c

    Обратите внимание, что порядок выполнения между b1, b2, b3 по-прежнему не определен. Приведу один хороший пример из предложения к стандарту:

    string s = 
      "but I have heard it works even if you don't believe in it";
    s.replace(0, 4, "")
    .replace(s.find("even"), 4, "only")
    .replace(s.find(" don't"), 6, "");
    assert(s == "I have heard it works only if you believe in it");

    Это код из книги Страуструпа «The C++ Programming Language, 4th edition», который использовался для демонстрации вызова методов «по цепочке». Ранее этот код имел unspecified behavior, однако начиная с C++17, он будет работать как и задумывалось. Дело в том, что неизвестно какая из функций find будет вызвана первой.

    Т.е. теперь в выражениях вида:

    obj.F1(subexr1).F2(subexr2).F3(subexr3).F4(subexr4)

    Подвыражения subexr1, subexr2, subexr3, subexr4 вычисляются согласно порядку вызова функций F1, F2, F3, F4. Ранее порядок вычисления таких подвыражений не был определен, что приводило к ошибкам.

    Filesystem


    C++17 предоставляет возможности для кроссплатформенной работы с файловой системой. Эта библиотека фактически является boost::filesystem, которую перенесли в стандарт.

    Рассмотрим несколько примеров работы с std::filesystem.

    Заголовочный файл и пространство имен:

    #include <filesystem>
    namespace fs = std::filesystem;

    Работа с объектом fs::path:

    fs::path file_path("/dir1/dir2/file.txt");
    cout << file_path.parent_path() << '\n'; // Выведет "/dir1/dir2"
    cout << file_path.filename() << '\n'; // Выведет "file.txt"
    cout << file_path.extension() << '\n'; // Выведет ".txt"
    
    file_path.replace_filename("file2.txt");
    file_path.replace_extension(".cpp");
    cout << file_path << '\n'; // Выведет "/dir1/dir2/file2.cpp"
    
    fs::path dir_path("/dir1");
    dir_path.append("dir2/file.txt");
    cout << dir_path << '\n'; // Выведет "/dir1/dir2/file.txt"

    Работа с директориями:

    // Получение текущей рабочей директории
    fs::path current_path = fs::current_path();
    
    // Создание директории
    fs::create_directory("/dir");
    
    // Создание нескольких директорий
    fs::create_directories("/dir/subdir1/subdir2");
    
    // Проверка существования директории
    if (fs::exists("/dir/subdir1"))
    {
      cout << "yes\n";
    }
    
    // Нерекурсивный обход директории
    for (auto &p : fs::directory_iterator(current_path))
    {
      cout << p.path() << '\n';
    }
    
    // Рекурсивный обход директории
    for (auto &p : fs::recursive_directory_iterator(current_path))
    {
      cout << p.path() << '\n';
    }
    
    // Нерекурсивное копирование директории
    fs::copy("/dir", "/dir_copy");
    
    // Рекурсивное копирование директории
    fs::copy("/dir", "/dir_copy", fs::copy_options::recursive);
    
    // Удаление директории со всем содержимым, если она существует
    fs::remove_all("/dir");

    Возможные значения fs::copy_options для обработки уже существующих файлов представлены в таблице:
    Константа Значение
    none Если файл уже существует, выбрасывается исключение. (Значение по умолчанию)
    skip_existing Существующие файлы не перезаписываются, исключение не выбрасывается.
    overwrite_existing Существующие файлы перезаписываются.
    update_existing Существующие файлы перезаписываются, только более новыми файлами.

    Работа с файлами:

    // Проверка существования файла
    if (fs::exists("/dir/file.txt"))
    {
      cout << "yes\n";
    }
    
    // Копирование файла
    fs::copy_file("/dir/file.txt", "/dir/file_copy.txt",
      fs::copy_options::overwrite_existing);
    // Получение размера файла (в байтах)
    uintmax_t size = fs::file_size("/dir/file.txt");
    
    // Переименование файла
    fs::rename("/dir/file.txt", "/dir/file2.txt");
    
    // Удаление файла, если он существует
    fs::remove("/dir/file2.txt");

    Это далеко не полный список возможностей std::filesystem. Со всеми возможностями можно ознакомиться здесь.

    std::optional


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

    // С++17
    std::optional<int> convert(my_data_type arg)
    {
      ....
      if (!fail)
      {
        return result;
      }
      return {};
    }
    
    int main()
    {
      auto val = convert(data);
      if (val.has_value())
      {
        std::cout << "conversion is ok, ";
        std::cout << "val = " << val.value() << '\n';
      }
      else
      {
        std::cout << "conversion failed\n";
      }
    
      return 0;
    }

    Еще у std::optional имеется метод value_or, который возвращает значение из optional, если оно доступно или иное установленное значение в противном случае.

    std::any


    Объект класса std::any может хранить информацию любого типа. Так, одна и та же переменная типа std::any может сначала хранить int, затем float, а затем строку. Пример:

    #include <string>
    #include <any>
    
    int main()
    {
      std::any a = 42;
      a = 11.34f;
      a = std::string{ "hello" };
      return 0;
    }

    Стоит отметить, что std::any не производит никаких привидений типов, что позволяет избежать неоднозначности. По этой причине, в примере явно указывается тип std::string, т.к. в противном случае в объекте std::any будет храниться простой указатель.

    Чтобы получить доступ к информации, хранящейся в объекте std::any, нужно воспользоваться std::any_cast. Например:

    #include <iostream>
    #include <string>
    #include <any>
    int main()
    {
      std::any a = 42;
      std::cout << std::any_cast<int>(a) << '\n';
    
      a = 11.34f;
      std::cout << std::any_cast<float>(a) << '\n';
    
      a = std::string{ "hello" };
      std::cout << std::any_cast<std::string>(a) << '\n';
    
      return 0;
    }

    Если в качестве шаблонного параметра std::any_cast был передан любой тип, отличный от типа текущего хранимого объекта, будет выброшено исключение std::bad_any_cast.

    Информацию о хранящемся типе можно получить с помощью метода type():

    #include <any>
    
    int main()
    {
      std::any a = 42;
      std::cout << a.type().name() << '\n'; // Напечатает "int"
    
      return 0;
    }

    std::variant


    std::variant — это шаблонный класс, который представляет собой union, который помнит, какой тип он хранит. Также, в отличие от union, std::variant позволяет хранить non-POD типы.

    #include <iostream>
    #include <variant>
    
    int main()
    {
      // хранит или int, или float или char.
      std::variant<int, float, char> v;
      v = 3.14f;
      v = 42;
      std::cout << std::get<int>(v);
      //std::cout << std::get<float>(v); // std::bad_variant_access
      //std::cout << std::get<char>(v); // std::bad_variant_access
      //std::cout << std::get<double>(v); // compile-error
      return 0;
    }

    Для получения значений из std::variant используется функция std::get. Она выбросит исключение std::bad_variant_access, если попытаться взять не тот тип.

    Также имеется функция std::get_if, которая принимает указатель на std::variant и возвращает указатель на текущее значение, если тип был указан правильно, и nullptr в противном случае:

    #include <iostream>
    #include <variant>
    
    int main()
    {
      std::variant<int, float, char> v;
      v = 42;
      auto ptr = std::get_if<int>(&v);
      if (ptr != nullptr)
      {
        std::cout << "int value: " << *ptr << '\n'; // int value: 42
      }
    
      return 0;
    }

    Обычно более удобным способом работы с std::variant является std::visit:

    #include <iostream>
    #include <variant>
    
    int main()
    {
      std::variant<int, float, char> v;
      v = 42;
    
      std::visit([](auto& arg)
      {
        using Type = std::decay_t<decltype(arg)>;
        if constexpr (std::is_same_v<Type, int>)
        {
          std::cout << "int value: " << arg << '\n';
        }
        else if constexpr (std::is_same_v<Type, float>)
        {
          std::cout << "float value: " << arg << '\n';
        }
        else if constexpr (std::is_same_v<Type, char>)
        {
          std::cout << "char value: " << arg << '\n';
        }
      }, v);
    
      return 0;
    }

    std::string_view


    В C++17 появился особый класс – std::string_view, который хранит указатель на начало существующей строки и ее размер. Таким образом, std::string_view представляет собой не владеющую памятью строку.

    У std::string_view имеются конструкторы, принимающие std::string, char[N], char*, поэтому больше нет необходимости писать 3 перегруженные функции:

    // C++14
    void Func(const char* str);
    void Func(const char str[10]);
    void Func(const std::string &str);
    
    // C++17
    void Func(std::string_view str);

    Теперь во всех функциях, принимающих const std::string&, можно изменить тип на std::string_view, поскольку это позволит повысить производительность для случаев, когда в функцию передается строковый литерал или Си-массив. Это связанно с тем, что при конструировании объекта std::string обычно происходит аллокация памяти, а при конструировании std::string_view никаких аллокаций, естественно, не происходит.

    Не стоит изменять тип аргумента функции с const string& на string_view только в том случае, если внутри этой функции вызывается функция с этим аргументом и принимающая const string&.

    try_emplace и insert_or_assign


    В C++17 у контейнеров std::map и std::unordered_map появились новые функции – try_emplace и insert_or_assign.

    В отличие от emplace, функция try_emplace не «крадёт» move-only аргумент, в случае если вставка элемента не произошла. Лучше всего объяснить это на примере:

    // C++17
    #include <iostream>
    #include <string>
    #include <map>
    
    int main()
    {
      std::string s1("hello");
      std::map<int, std::string> myMap;
      myMap.emplace(1, "aaa");
      myMap.emplace(2, "bbb");
      myMap.emplace(3, "ccc");
    
      //std::cout << s1.empty() << '\n'; // 0
      //myMap.emplace(3, std::move(s1));
      //std::cout << s1.empty() << '\n'; // 1
    
      //std::cout << s1.empty() << '\n'; // 0
      //myMap.try_emplace(3, std::move(s1));
      //std::cout << s1.empty() << '\n'; // 0
    
      std::cout << s1.empty() << '\n'; // 0
      myMap.try_emplace(4, std::move(s1));
      std::cout << s1.empty() << '\n'; // 1
    
      return 0;
    }

    Если вставка не происходит, из-за того, что элемент с таким ключом уже есть в myMap, try_emplace не «крадёт» строку s1, в отличие от emplace.

    Функция insert_or_assign вставляет элемент в контейнер, если элемента с таким ключом еще не нет в контейнере и перезаписывает существующий элемент, если элемент с таким ключом существует. Функция возвращает std::pair, состоящий из итератора на вставленный/перезаписанный элемент и булевого значения, показывающего произошла вставка нового элемента или нет. Таким образом эта функция аналогична operator[], но возвращает дополнительную информацию о том, была выполнена вставка или перезапись элемента:

    // C++17
    #include <iostream>
    #include <string>
    #include <map>
    
    int main()
    {
      std::map<int, std::string> m;
      m.emplace(1, "aaa");
      m.emplace(2, "bbb");
      m.emplace(3, "ccc");
    
      auto[it1, inserted1] = m.insert_or_assign(3, "ddd");
      std::cout << inserted1 << '\n'; // 0
    
      auto[it2, inserted2] = m.insert_or_assign(4, "eee");
      std::cout << inserted2 << '\n'; // 1
    
      return 0;
    }

    До C++17 чтобы выяснить, произошла вставка или обновление приходилось сначала искать элемент, а затем применять operator[].

    Специальные математические функции


    В C++17 было добавлено множество специализированных математических функций, таких как: бета-функции, Дзета-функции Римана и прочие. Подробнее о них прочитать можно здесь.

    Объявление вложенных пространств имен


    В C++17 можно написать:

    namespace ns1::ns2
    {
      ....
    }

    Вместо:

    namespace ns1
    {
      namespace ns2
      {
        ....
      }
    }

    Неконстантный string::data


    В C++17 у std::string появился метод data(), возвращающий неконстантный указатель на внутренние данные строки:

    // С++17
    #include <iostream>
    
    int main()
    {
      std::string str = "hello";
      char *p = str.data();
      p[0] = 'H';
      std::cout << str << '\n'; // Hello
    
      return 0;
    }

    Это будет полезно при работе со старыми Си библиотеками.

    Параллельные алгоритмы


    У функций из <algorithm>, работающих с контейнерами, появились многопоточные версии. Все они получили дополнительную перегрузку, принимающую первым аргументом execution policy, который определяет то, каким образом будет выполняться алгоритм.

    Execution policy может принимать одно из 3-х значений:

    1. std::execution::seq – последовательное выполнение
    2. std::execution::par – параллельное выполнение
    3. std::execution::par_unseq – параллельное векторизованное выполнение

    Таким образом, чтобы получить многопоточную версию алгоритма, достаточно написать:

    #include <iostream>
    #include <vector>
    #include <algorithm>
    ....
    std::for_each(std::execution::par, vct.begin(), vct.end(),
      [](auto &e) { e += 42; });
    ....

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

    Также стоит отметить разницу между std::execution::seq и версией без такого параметра – если в функцию передается execution policy, то в этом алгоритме нельзя выбрасывать исключения, которые выходят за границы функтора. Если выбросить такое исключение, будет вызван std::terminate.

    В связи с добавлением параллелизма, появилось несколько новых алгоритмов:

    std::reduce – работает аналогично std::accumulate, но порядок свертки строго не определен, поэтому может работать параллельно. Имеет перегрузку, принимающую execution policy. Небольшой пример:

    ....
    // Суммируем все элементы vct в параллельном режиме
    std::reduce(std::execution::par, vct.begin(), vct.end())
    ....

    std::transform_reduce – применяет заданный функтор на элементах контейнера, а затем применяет std::reduce.

    std::for_each_n – работает аналогично std::for_each, но заданный функтор применяется только к n элементам. Например:

    ....
    std::vector<int> vct = { 1, 2, 3, 4, 5 };
    std::for_each_n(vct.begin(), 3, [](auto &e) { e *= 10; });
    // vct: {10, 20, 30, 4, 5}
    ....

    std::invoke, трейт is_invocable


    std::invoke принимает на вход сущность, которая может быть вызвана, и набор аргументов и вызывает эту сущность с этими аргументами. Такими сущностями, например, являются указатель на функцию, объект с operator(), лямбда-функция и прочие:

    // C++17
    #include <iostream>
    #include <functional>
    
    int Func(int a, int b)
    {
      return a + b;
    }
    
    struct S
    {
      void operator() (int a)
      {
        std::cout << a << '\n';
      }
    };
    
    int main()
    {
      std::cout << std::invoke(Func, 10, 20) << '\n'; // 30
      std::invoke(S(), 42); // 42
      std::invoke([]() { std::cout << "hello\n"; }); // hello
    
      return 0;
    }

    std::invoke может пригодиться в какой-нибудь шаблонной магии. Также в C++17 был добавлен трейт std::is_invocable:

    // C++17
    #include <iostream>
    #include <type_traits>
    
    void Func() { };
    
    int main()
    {
      std::cout << std::is_invocable<decltype(Func)>::value << '\n'; // 1
      std::cout << std::is_invocable<int>::value << '\n'; // 0
    
      return 0;
    }

    std::to_chars, std::from_chars


    В C++17 появились функции std::to_chars и std::from_chars для очень быстрого преобразования чисел в строки и строк в числа соответственно. В отличие от других функций форматирования из C и C++, std::to_chars не зависит от локали, не выделяет память и не выбрасывает исключений, и нацелены на максимальную производительность:

    // C++17
    #include <iostream>
    #include <charconv>
    
    int main()
    {
      char arr[128];
      auto res1 = std::to_chars(std::begin(arr), std::end(arr), 3.14f);
      if (res1.ec != std::errc::value_too_large)
      {
        std::cout << arr << '\n';
      }
    
      float val;
      auto res2 = std::from_chars(std::begin(arr), std::end(arr), val);
      if (res2.ec != std::errc::invalid_argument &&
          res2.ec != std::errc::result_out_of_range)
      {
        std::cout << arr << '\n';
      }
    
      return 0;
    }

    Функция std::to_chars возвращает структуру to_chars_result:

    struct to_chars_result
    {
      char* ptr;
      std::errc ec;
    };

    ptr – указатель на последний записанный символ + 1

    ec – код ошибки

    Функция std::from_chars возвращает структуру from_chars_result:

    struct from_chars_result 
    {
      const char* ptr;
      std::errc ec;
    };

    ptr – указатель на первый символ, не удовлетворяющий паттерну

    ec – код ошибки

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

    std::as_const


    Вспомогательная функция std::as_const принимает на вход ссылку и возвращает ссылку на константу:

    // C++17
    #include <utility>
    ....
    MyObject obj{ 42 };
    const MyObject& constView = std::as_const(obj);
    ....

    Свободные функции std::size, std::data и std::empty


    В дополнение к уже существующим свободным функциям std::begin, std::end и прочим появились свободные функции std::size, std::data и std::empty:

    // C++17
    #include <vector>
    
    int main()
    {
      std::vector<int> vct = { 3, 2, 5, 1, 7, 6 };
    
      size_t sz = std::size(vct);
      bool empty = std::empty(vct);
      auto ptr = std::data(vct);
    
      int a1[] = { 1, 2, 3, 4, 5, 6 };
      // стоит использовать для C-style массивов.
    
      size_t sz2 = std::size(a1);
      return 0;
    }

    std::clamp


    В C++17 появилась функция std::clamp(x, low, high), которая возвращает x, если он находится в интервале [low, high] или ближайшее из этих значений в противном случае:

    // C++17
    #include <iostream>
    #include <algorithm>
    
    int main()
    {
      std::cout << std::clamp(7, 0, 10) << '\n'; // 7
      std::cout << std::clamp(7, 0, 5) << '\n'; //5
      std::cout << std::clamp(7, 10, 50) << '\n'; //10
    
      return 0;
    }

    НОД и НОК


    В стандарте появилось вычисление Наибольшего Общего Делителя (std::gcd) и Наименьшего Общего Кратного (std::lcm):

    // C++17
    #include <iostream>
    #include <numeric>
    
    int main()
    {
      std::cout << std::gcd(24, 60) << '\n'; // 12
      std::cout << std::lcm(8, 10) << '\n'; // 40
    
      return 0;
    }

    Логические метафункции (Logical operation metafunctions)


    В C++17 появились логические метафункции std::conjunction, std::disjunction и std::negation. Они используются для того, чтобы выполнить логическое И, ИЛИ, НЕ на наборе трейтов соответственно. Небольшой пример с std::conjunction:

    // C++17
    #include <iostream>
    #include <string>
    #include <algorithm>
    #include <functional>
    
    template<typename... Args>
    std::enable_if_t<std::conjunction_v<std::is_integral<Args>...>>
    Func(Args... args)
    {
      std::cout << "All types are integral.\n";
    }
    
    template<typename... Args>
    std::enable_if_t<!std::conjunction_v<std::is_integral<Args>...>>
    Func(Args... args)
    {
      std::cout << "Not all types are integral.\n";
    }
    
    int main()
    {
      Func(42, true); // All types are integral.
      Func(42, "hello"); // Not all types are integral. 
    
      return 0;
    }

    Замечу, что в отличие от свертки параметров шаблона, упомянутой выше, функции std::conjunction и std::disjunction остановят инстанцирование, как только результирующее значение сможет быть определено.

    Атрибуты в пространствах имен и перечислениях


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

    // C++17
    #include <iostream>
    
    enum E
    {
      A = 0,
      B = 1,
      C = 2,
      First[[deprecated]] = A,
    };
    
    namespace[[deprecated]] DeprecatedFeatures
    {
      void OldFunc() {};
    //....
    }
    
    int main()
    {
      // Будет выдано предупреждение компилятора
      DeprecatedFeatures::OldFunc();
      
      // Будет выдано предупреждение компилятора
      std::cout << E::First << '\n'; 
    
      return 0;
    }

    Префикс using для атрибутов


    Добавлен префикс using для атрибутов, поэтому при использовании нескольких атрибутов можно немного сократить запись. Пример из предложения к стандарту (P0028R4):

    // C++14
    void f() 
    {
      [[rpr::kernel, rpr::target(cpu, gpu)]]
      task();
    }
    
    // C++17
    void f() 
    {
      [[using rpr:kernel, target(cpu, gpu)]]
      task();
    }

    Возвращаемое значение у emplace_back


    Теперь emplace_back возвращает ссылку на вставленный элемент, до C++17 он не возвращал никакого значения:

    #include <iostream>
    #include <vector>
    
    int main()
    {
      std::vector<int> vct = { 1, 2, 3 };
    
      auto &r = vct.emplace_back(10);
      r = 42;
    
      for (const auto &i : vct)
      {
        std::cout << i << ' ';
      }
    }

    Функторы для поиска подстроки в строке (Searcher functors)


    В C++17 появились функторы, реализующие поиск подстроки в строке, использующие алгоритм Бойера – Мура или алгоритм Бойера — Мура – Хорспула. Эти функторы можно передавать в std::search:

    #include <iostream>
    #include <string>
    #include <algorithm>
    #include <functional>
    
    int main()
    {
      std::string haystack = "Hello, world!";
      std::string needle = "world";
    
      // Стандартный поиск
      auto it1 = std::search(haystack.begin(), haystack.end(),
        needle.begin(), needle.end());
    
      auto it2 = std::search(haystack.begin(), haystack.end(),
        std::default_searcher(needle.begin(), needle.end()));
    
      // Поиск с использованием алгоритма Бойера - Мура
      auto it3 = std::search(haystack.begin(), haystack.end(),
        std::boyer_moore_searcher(needle.begin(), needle.end()));
    
      // Поиск с использованием алгоритма Бойера - Мура - Хорспула
      auto it4 = std::search(haystack.begin(), haystack.end(),
        std::boyer_moore_horspool_searcher(needle.begin(), needle.end()));
    
      std::cout << it1 - haystack.begin() << '\n'; // 7
      std::cout << it2 - haystack.begin() << '\n'; // 7
      std::cout << it3 - haystack.begin() << '\n'; // 7
      std::cout << it4 - haystack.begin() << '\n'; // 7
    
      return 0;
    }

    std::apply


    std::apply вызывает сallable-объект с набором параметров, записанным в кортеже. Пример:

    #include <iostream>
    #include <tuple>
    
    void Func(char x, int y, double z)
    {
      std::cout << x << y << z << '\n';
    }
    
    int main()
    {
      std::tuple args{ 'c', 42, 3.14 };
      std::apply(Func, args);
    
      return 0;
    }

    Конструирование объектов из кортежей (std::make_from_tuple)


    В C++17 появилась возможность сконструировать объект, передав в конструктор набор аргументов, записанных в кортеже. Для этого используется функция std::make_from_tuple:

    #include <iostream>
    #include <tuple>
    
    struct S
    {
      char m_x;
      int m_y;
      double m_z;
      S(char x, int y, double z) : m_x(x), m_y(y), m_z(z) {}
    };
    
    int main()
    {
      std::tuple args{ 'c', 42, 3.14 };
      S s = std::make_from_tuple<S>(args);
      std::cout << s.m_x << s.m_y << s.m_z << '\n';
    
      return 0;
    }

    std::not_fn (Universal negator not_fn)


    В C++17 появилась функция std::not_fn, возвращающая предикат-отрицание. Эта функция призвана заменить std::not1 и std::not2:

    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <functional>
    
    bool LessThan10(int a)
    {
      return a < 10;
    }
    
    int main()
    {
      std::vector vct = { 1, 6, 3, 8, 14, 42, 2 };
    
      auto n = std::count_if(vct.begin(), vct.end(),
        std::not_fn(LessThan10)); 
     
      std::cout << n << '\n'; // 2
    
      return 0;
    }

    Доступ к нодам контейнеров (Node handle)


    В С++17 появилась возможность перемещать ноду напрямую из одного контейнера в другой. При этом не происходят дополнительные аллокации или копирование. Приведу небольшой пример:

    // C++17
    #include <map>
    #include <string>
    
    int main()
    {
      std::map<int, std::string> myMap1{ { 1, "aa" },
                                         { 2, "bb" },
                                         { 3, "cc" } };  
      std::map<int, std::string> myMap2{ { 4, "dd" },
                                         { 5, "ee" },
                                         { 6, "ff" } };
      auto node = myMap1.extract(2);
      myMap2.insert(std::move(node));
     
      // myMap1: {{1, "aa"}, {3, "cc"}}
      // myMap2: {{2, "bb"}, {4, "dd"}, {5, "ee"}, {6, "ff"}}
    
      return 0;
    }

    Метод std::extract позволяет извлечь ноду из контейнера, а метод insert теперь также умеет вставлять ноды.

    Также в C++17 у контейнеров появился метод merge, который пытается извлечь все ноды контейнера с помощью extract и вставить их в другой контейнер с помощью insert:

    // C++17
    #include <map>
    #include <string>
    
    int main()
    {
      std::map<int, std::string> myMap1{ { 1, "aa" },
                                         { 2, "bb" },
                                         { 3, "cc" } };
                                         
      std::map<int, std::string> myMap2{ { 4, "dd" },
                                         { 5, "ee" },
                                         { 6, "ff" } };
      myMap1.merge(myMap2);
      // myMap1: {{1, "aa"},
      //          {2, "bb"},
      //          {3, "cc"},
      //          {4, "dd"},
      //          {5, "ee"},
      //          {6, "ff"}}
      // myMap2: {}
    
      return 0;
    }

    Еще одним интересным примером может служить изменение ключа элемента в std::map:

    // C++17
    #include <map>
    #include <string>
    
    int main()
    {
      std::map<int, std::string> myMap{ { 1, "Tommy" },
                                        { 2, "Peter" },
                                        { 3, "Andrew" } };
      auto node = myMap.extract(2);
      node.key() = 42;
      myMap.insert(std::move(node));
    
      // myMap: {{1, "Tommy"}, {42, "Peter"}, {3, "Andrew"}};
    
      return 0;
    }

    До C++17 избежать дополнительных накладных расходов при изменении ключа было невозможно.

    static_assert с одним аргументом


    Теперь для static_assert необязательно указывать сообщение:

    static_assert(a == 42, "a must be equal to 42");
    static_assert(a == 42); // Теперь можно писать так
    static_assert ( constant-expression ) ;
    static_assert ( constant-expression , string-literal ) ;

    std::*_v<T...>


    В C++17 у всех трейтов из <type_traits>, имеющих поле ::value, появились перегрузки вида some_trait_v<T>. Поэтому теперь вместо того, чтобы писать some_trait<T>::value, можно просто написать some_trait_v<T>. Например:

    // C++14
    static_assert(std::is_integral<T>::value, "Integral required.");
    
    // C++17
    static_assert(std::is_integral_v<T>, "Integral required");

    std::shared_ptr for arrays


    Теперь shared_ptr поддерживает C-массивы. Необходимо просто передать T[] шаблонным параметром и shared_ptr вызовет delete[] при освобождении памяти. Ранее для массивов нужно было указывать функцию для удаления вручную. Небольшой пример:

    #include <iostream>
    #include <memory>
    
    int main()
    {
      // C++14
      //std::shared_ptr<int[]> arr(new int[7],
      //  std::default_delete<int[]>());
    
      // C++17
      std::shared_ptr<int[]> arr(new int[7]);
    
      arr.get()[0] = 1;
      arr.get()[1] = 2;
      arr.get()[2] = 3;
      ....
    
      return 0;
    }

    std::scoped_lock


    В C++17 появился новый класс scoped_lock, который блокирует несколько мьютексов одновременно (используя lock) при создании и освобождает их всех в деструкторе, предоставляя удобный RAII-интерфейс. Небольшой пример:

    #include <thread>
    #include <mutex>
    #include <iostream>
    
    int var;
    std::mutex varMtx;
    
    void ThreadFunc()
    {
      std::scoped_lock lck { varMtx };
      var++;
      std::cout << std::this_thread::get_id() << ": " << var << '\n';
    } // <= varMtx автоматически освобождается при выходе из блока
    
    int main()
    {
      std::thread t1(ThreadFunc);
      std::thread t2(ThreadFunc);
    
      t1.join();
      t2.join();
    
      return 0;
    }

    Удаленные возможности


    • Были удалены триграфы.
    • Ключевое слово register больше нельзя использовать как спецификатор переменной. Оно остается зарезервированным на будущее, как это было с auto.
    • Были удалены префиксный и постфиксный инкременты для типа bool.
    • Была удалена спецификация исключений. Больше нельзя указать какие именно исключения выбрасывает функция. В C++17 стоит лишь помечать функции, которые не выбрасывают исключений как noexcept.
    • Был удален std::auto_ptr. Вместо него стоит использовать std::unique_ptr.
    • Был удален std::random_shuffle. Вместо него стоит использовать std::shuffle, с соответствующим функтором, генерирующим случайные числа. Удаление связанно с тем, что std::random_shuffle использовал std::rand, который в свою очередь признан устаревшим.

    Итоги


    К сожалению, в C++17 не вошли ожидаемые всеми модули, концепты, работа с сетью, рефлексия и прочие важные фичи, поэтому с нетерпением ждем C++20.

    Для себя, как одного из разработчиков анализатора кода PVS-Studio, могу отметить, что нам предстоит много интересной работы. Новые возможности языка открывают и новые возможности «отстрелить себе ногу», и мы должны научить анализатор предупреждать программиста об ошибках новых разновидностей. Например, в C++14 появилась возможность инициализировать динамический массив при его создании. Следовательно, полезно предупреждать программиста, когда размер динамического массива может оказаться меньше количества элементов в его инициализаторе. Поэтому мы создали новую диагностику V798. Диагностики для новых конструкций языка мы делали и продолжаем делать. Для C++17 будет полезно, например, предупредить, что в алгоритме для std::execution::par используются конструкции, которые могут сгенерировать исключения и эти исключения не будут специально перехвачены внутри алгоритма с помощью try...catch.

    Спасибо за внимание. Предлагаю скачать PVS-Studio (Windows/Linux) и проверить свои проекты. Язык C++ становится все «более большим» и все сложнее отследить все аспекты и нюансы его использования, чтобы написать правильный код. PVS-Studio содержит большую базу знаний о том, «что делать нельзя» и будет вам незаменимым помощником. Да и от простых опечаток никто не застрахован и никуда эта проблема не денется. Proof.

    Дополнительные ссылки


    1. Changes between C++14 and C++17 DIS.
    2. Youtube. Полухин Антон | C++17.
    3. Youtube. Nicolai Josuttis. С++17. The Language Features. Part 1, Part 2.
    4. Herb Sutter. Trip report: Summer ISO C++ standards meeting (Oulu).
    5. Bartlomiej Filipek. C++ 17 Features.




    Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Egor Bredikhin. C++17

    Прочитали статью и есть вопрос?
    Часто к нашим статьям задают одни и те же вопросы. Ответы на них мы собрали здесь: Ответы на вопросы читателей статей про PVS-Studio, версия 2015. Пожалуйста, ознакомьтесь со списком.
    PVS-Studio 369,59
    Ищем ошибки в C, C++ и C# на Windows и Linux
    Поделиться публикацией
    Комментарии 176
    • +31
      скоро::программировать<'на'> &зыке << !С++ ^будут* << _так?;
      • +4
        это ведь не случайно, что у вас написано кириллицей? Импортозамещение и все дела?
        • +2
          Ну, C# позволяет писать идентификаторы любыми символами, которые в стандарте unicode определены как Letter, почему бы в C++ не принять эту фичу.
          • +2
            Тут возникает ряд существенных граблей.
            Например, обязан ли язык различать Á (U+00C1) и Á (U+0041 U+0301)? Python различает, но приведение всех идентификаторов к NKFC может быть дороговато (в компилятор втягивать что-то размера ICU, причём на этапе, где лексер и так отрабатывает много специфики языка — см. например Clang — где есть лексический парсинг обычного кода, строк препроцессора, кода внутри блока `#if 0`, и т.п.) А вот Go ничего не делает с этим — и можно получить злобные проблемы от внешне одинаковых идентификаторов в разных местах, так, что редактор этого не покажет.
            А когда апдейтится стандарт Unicode — менять таблицы вслед?
            Требовать ли от внешних средств типа линкера поддержки таких идентификаторов, и что делать, если не поддерживает?

            Я не согласен со многими решениями в C++, но сама идея, что это (вместе с C) всё-таки языки системного программирования, предполагает, что выход за пределы базовых доступных везде средств должен выполняться очень ограниченно и осмотрительно.

            Ведь даже наличие ASCII, считаем, стало обязательным только с C++17 — я имею в виду отказ от триграфов, которые применялись для возможности написания на C++ в странных местах типа zSeries с локализованными вариантами EBCDIC…
            • +3
              В случае с C++ всё просто: ничего никуда не конвертируется. В стандарте просто сказано: An identifier is an arbitrarily long sequence of letters and digits. Each universal-character-name in an identifier shall designate a character whose encoding in ISO 10646 falls into one of the ranges specified in E.1.

              И компиляторы, хотя и не все, давно позволяют называет идентификаторы по русски. Это вопрос к программистам, а не к стандарту сегодня, на самом деле.
            • +3
              почему бы в C++ не принять эту фичу
              А это от программистов зависит, компиляторы её ужа давно поддерживают.
          • +35
            1С++17?
          • +2
            Спасибо за статью.
            • –13
              Чувствуется влияние Golang и Rust.
              • +10
                <sarcasm>особенно golang</sarcasm>
                • –5
                  Только не включайте больше ваш сарказм, пожалуйста. А просто перечитайте про инициализаторы в if и switch.
                  • 0
                    Возможность «инициализатор в if и switch» — это как раз если не впервые в таком виде появилось в Go, то по крайней мере там стало хорошо известно — и доказало свою полезность.
                    Или Вы знаете другой язык из популярных, где это было раньше?
                    • +8
                      Инициализаторы в if в C++ были очень давно. Не хватало возможности определить условие, отличное от приведения проинициализированной переменной к bool.
                      • –4
                        Ну то есть они были практически бесполезны.
                        А вот именно в такой удобной форме (и с блоком на весь if/switch) — образцом послужил Go.
                        • +6
                          С указателями получалось очень даже красиво:
                          if (foo* p = findFoo(id)) { p->bar(); }

                          • +4
                            Вообще-то образцов послужил C++98, в котором это было разрешено делать в цикле for.

                            Когда эта фича в C++ появилась — ни rust'а, ни go даже в проекте не было.
                        • 0
                          if (y = f(x), y > x) {
                              ... // statements involving x and y
                          }
                          
                          • 0
                            новый синтаксис — просто развитие идеи.
                            • +1
                              Упускаете главный момент.
                              Новая форма аналогична созданию блока, в котором определены новые переменные, а после этого вложен собственно if. Пример автора статьи:

                              if (auto it = m.find(key); it != m.end())
                              {
                                ....
                              }
                              


                              аналогичен

                              {
                                auto if = m.find(key);
                                if (it != m.end())
                                {
                                  ....
                                }
                                // кстати, тут может быть else-ветка, или даже цепочка else if ... else,
                                // в которой эта переменная будет видна
                              }
                              


                              но блок не выписывается явно.
                              По выходу из блока сработают деструкторы; кроме того, новая переменная не будет видна после завершения if — чтобы случайно её не применить где не следует.

                              Потому — это чисто «сахар». Но очень практически полезный, раз ввели.

                              Кстати, насколько я понял final draft, несколько отдельных init-statement ввести нельзя. Немного жаль.
                              • +1
                                Я с этим и не спорю. С моей точки зрения это никак не связано с go, все совпадения с go случайны :).
                                • –1
                                  А Вы где-то ещё видели именно такой же синтаксис — две части через точку с запятой, первая необязательна, но должна что-то инициализировать? Я — только в одном источнике, и его тут уже назвали.
                                  Был бы другой образец — взяли бы его, потому что ничто не мешало, например, создать отдельное ключевое слово и блок за ним (что было бы как-то более понятно при отсутствии такого образца), или сделать другое построение блока (хм, а почему if и switch, но не while?), перетащить GCC вариант ({...}) с уточнением блочного контекста, и т.п.
                                  • +3
                                    ну, например, тут:
                                    for(int i=f(x); i<10;) {;;}

                                    распространение этого синтаксиса и на if является вполне логичным продолжением даже без влияния go. Тот факт, что что-то в новом C++ похоже на go ещё не означает, что они двигаются в одном направлении и даже не значит, что именно оттуда оно заимствуется:
                                    Цитирую пропозал:
                                    There are three statements in C++, if, for and while, which are all variations on a theme. We propose to make the picture more complete by adding a new form of if statement.
                                    • –3
                                      > распространение этого синтаксиса и на if является вполне логичным продолжением даже без влияния go.

                                      Для меня тут колоссальной разницей является то, что в for три части присутствуют обязательно (даже если пустые), а в if первая может отсутствовать. Именно конкретный метод решения был взят в конкретном месте. Альтернатива, в виде названного в том же пропозале with(), присутствовала в других местах.

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

                                      > There are three statements in C++, if, for and while, which are all variations on a theme.

                                      И он тут же исключает из рассмотрения else для if… мне эта логика ой не кажется корректной.

                                      К слову, else для for и while (по образцу Питона) иногда тоже очень полезно. :)
                                    • +1
                                      А Вы где-то ещё видели именно такой же синтаксис — две части через точку с запятой,
                                      А какое это имеет отношение к делу? После не значит в следствии. Мысль о том, что if не хватает инициализации посещала меня ещё до появления golang. Да и сами авторы пропозала тоже мотивируют внутренними потребностями языка.
                        • +7
                          Вот от этого фрагмента прям какой-то теплой ламповой сишечкой повеяло:
                          auto res1 = std::to_chars(arr, arr + 128, 3.14f);
                          Может имело бы смысл его записать в духе современного C++?
                          auto res1 = std::to_chars(std::begin(arr), std::end(arr), 3.14f);
                          Как-то понадежнее будет.
                          • +2
                            Да, согласен, спасибо. Подправил в статье.
                            • +1
                              Очень странно что to_chars возвражает структуру, а не std::pair
                              Тогда бы и тут можно было писать что-то типа
                              auto [ptr, error] = std::to_chars(...)
                              • +5
                                Так написать можно. Structured bindings, как было сказано в статье, умеют производить декомпозицию типов, содержащих только нестатические открытые члены.
                            • +4

                              Меня больше бомбануло от


                              struct to_chars_result
                              {
                                char* ptr;
                                std::errc ec;
                              };

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

                              • +4
                                На вход to_chars подаются тоже указатели. А вообще, to_chars была так спроектированна именно для максимально быстрого преобразования.
                                • +3

                                  Тогда могли бы через string_view. И возвращать тоже часть string_view. Увы, много рассказывают про "безопасные практики" — при этом периодически добавляя способов отстрелить себе ногу.

                                • +1
                                  Ну, вообще-то, to_chars получает на вход char*. Что получил, то и вернул.

                                  Другое дело, что к to_chars_result::ptr можно получить доступ даже если в to_chars_result::ec находится код ошибки… Вот это не есть хорошо. Какой-нибудь std::variant<char*, std::errc> был бы, наверное, уместнее. Но, вероятно, с точки зрения эффективности to_chars_result обходится дешевле.
                                  • +1
                                    Вот тут меня тож переклинило. Зачем спрашивается std::optional?
                                    • +3
                                      std::optional не поможет вернуть код ошибки в случае неудачи. А вот аналог Rust-овского Result-а в стандартной библиотеке бы не помешал.
                                      • +1
                                        Поверх std::variant можно сделать Result/Either. Даже с монадической структурой (только вместо >>= придётся писать >>, так как >>= правоассоциативен).

                                        Но было бы приятно, если бы это было в стандартной библиотеке, да.
                            • +2
                              Когда уже откажутся от header-файлов?
                              • +5
                                Когда завезут модульную систему + лет 10 (а может и больше) на избавление от легаси.
                                • +1

                                  А что в них настолько плохого, что от них нужно обязательно избавиться?


                                  PS Интересно, а что мешает писать их не используя?

                                  • +3
                                    Время компиляции в них плохое. При каждом include парсятся многомегабайтные заголовки библиотек. А как вы предлагаете их не использовать? Везде писать extern?
                                    • +7

                                      Многомегабайтные заголовки сами по себе не такая большая беда. Компиляторы довольно давно научились это дело кешировать. Две гораздо большие проблемы — война макросов и свалка определений.


                                      Война макросов, как можно догадаться — конфликт и наложение эффектов от макросов, определённых в разных заголовках.


                                      Свалка определений — тащим одну маленькую шаблонную функцию, а получаем в область видимости пол-буста вместе с MPL. К тому же, приводит к необходимости тащить вместе с библиотекой заголовки всех её зависимостей (и правильно их раскладывать по местам), которые отметились в заголовках самой библиотеки. И транзитивные в том числе.

                                    • +5
                                      Я просто перешел на C#.
                                      • +4

                                        В C# уже завезли RAII и нативную компиляцию?

                                        • 0

                                          Да, в C# с первых версий завезена RAII, просто он требует большего числа ручных действий.

                                          • +2
                                            using никак не сравнится по гибкости и удобству с RAII в С++
                                            • 0
                                              using это просто синтаксический сахар над finally { .Dispose(); } и ничего более.
                                              • 0
                                                Да, я в курсе. Был бы RAII также любим если бы нужно было писать obj.~Object(); каждый раз?)
                                                • 0
                                                  То есть, разрушение C++ объектов при выходе из скоупа — тоже сахар?
                                          • +2
                                            Точно так же можно сказать: «да, в С (без плюсов) первых версий завезена RAII, просто он требует большего числа ручных действий».
                                            • +3
                                              В ассемблер завезена RAII, просто чуть больше ручных действий.
                                          • 0
                                            ru.wikipedia.org/wiki/Ngen
                                            По сути дела, нативная компиляция.
                                            • 0

                                              А насколько он сравним с С++ по эффективности генерируемого кода?

                                              • 0
                                                Попробуйте поискать тесты, я не интересовался этим вопросом.

                                                Теоретически, на некоторых задачах даже обгонять может (по аналогии с оптимизирующими JIT-компиляторами — встречал подобные тесты для JVM, например), т.к. бинарь компилируется непосредственно под имеющуюся известную процессорную архитектуру/поколение, следовательно, можно сразу очень эффективно оптимизировать код.
                                                • 0
                                                  Все компиляторы C++ умеют компилировать под имеющуюся известную процессорную архитектуру/поколение
                                                  • 0
                                                    В дистрибутивах, где всё собирается из исходников под свою машину, традиционные компиляторы всегда лучше jit.
                                                    • 0
                                                      Само собой. Но хороший профит из этого можно извлечь только при условии, что у вас есть возможность собирать ваш софт непосредственно на машине у конечного пользователя (как принято в некоторых Linux-дистрибутивах), или если программа пишется под конкретный ограниченный круг железа. При дистрибьюции же бинаря «широкой публике», увы, с просторами для оптимизаций все не так прекрасно.
                                                      • 0
                                                        Горячие функции можно собирать под несколько разных микроархитектур или доступных SIMD и в рантайме один раз выбирать нужную.
                                                        • 0
                                                          Но это полумера. Всегда есть граница между «горячими» и «обычными» фунциями. Причём, если разбиение проводить вручную, могут быть и ошибки, и слишком завышенные критерии горячих функций. Некоторые функции, близкие к границе, уже не попадут в «горячие», как могли бы при полной компиляции под целевую архитектуру или при jit-компиляции.
                                                          • 0
                                                            Некоторые функции, близкие к границе, уже не попадут в «горячие», как могли бы при полной компиляции под целевую архитектуру или при jit-компиляции.
                                                            Понятно, что сборку строго под конкретную платформу вы не победите.

                                                            С JIT'ом же всё не так однозначно: да, он лучше отбирает функции, чем рачная подстройка — но и ресурсов он на себя отбирает больше! Не факт, что в результате выигрыш получится.

                                                            То есть создать бенчмарк под любой JIT, чтобы показать его «крутизну» — не проблема. А вот в реальных приложениях… не факт, ой как не факт…
                                                            • 0
                                                              А в реальных приложениях — обычно увы и ах.
                                        • +2
                                          Когда уже откажутся от header-файлов?

                                          Когда завезут модульную систему

                                          Свалка определений — тащим одну маленькую шаблонную функцию, а получаем в область видимости пол-буста вместе с MPL.


                                          Я просто перешел на C#.

                                          А я — никуда уходил с Delphi, там всего этого 'счастья' не было изначально.
                                          • +1

                                            Поддерживаю, в современных Delphi анонимные функции и обобщенные типы (generics) намного прятнее всех этих templates и deduction rules.

                                        • 0
                                          даже если спецификатор constexpr не указан, лямбда все равно будет constexpr, если это возможно


                                          Зачем же тогда указывать constexpr? Явная декларация о намерениях? Мне кажется, С++ движется в этом смысле в сторону питона — explicit is better than implicit.
                                          • 0
                                            Постоянное дежавю с питоном. Похоже, в комитет проникли питонисты

                                            auto[iter, ok] = mySet.insert(42);
                                            Python: a,b=1,2
                                            • +3
                                              Скорее с хаскелем. Структуру вы так в питоне не разложите.
                                              • +1
                                                Структура в Python это namedtuple — очень даже разложится.
                                              • 0
                                                Ну вообще-то концепция, по которой у функции всегда предполагалось только одно значение-результат, выглядит устаревшей. Она была хороша в период ранней «математизации» понятия функции (особенно по сравнению с subroutine в Fortran, где функции обязаны были быть «чистыми»), но сейчас нет смысла добровольно вжиматься в прокрустово ложе. Тем более что примеров, когда реально передаётся несколько значений, но все кроме одного идут косвенными каналами — полно в любом системном API.

                                                А с чем сравнивать возврат нескольких значений — с Python, Go, Swift, Erlang, Haskell, чем-то ещё — вопрос персонального опыта. В данном случае второй ok это ближе к тому, что я видел по Go. Был бы он первым — был бы стиль Erlang :)
                                                • 0
                                                  Чем это принципиально отличается от ссылочных out-параметров?
                                                  • 0
                                                    Чтобы ответить на этот вопрос, мне нужно знать уровень Вашей «принципиальности» подхода к этой разнице. Но есть два основных аспекта:
                                                    1. Формальный — что это именно части полного результата функции и соответственно не требуют рисования промежуточных переменных.
                                                    2. Практический — что в точно такой же манере, как современные ABI предпочитают передавать K первых параметров через регистры, чтобы гонять через RAM — результаты тоже можно передавать через регистры и не заниматься косвенным доступом.
                                                    Ну а насколько Вам это будет принципиально — не могу предсказать.
                                                  • +1
                                                    Самое забавное, что в хаскеле возвращается-то всё равно ровно одно значение, просто оно может быть туплом.
                                                • 0
                                                  На мой взгляд, минус constexpr в том, что он все равно не гарантирует выполнения на этапе компиляции. Вполне можно было вообще все функции считать constexpr по-умолчанию, а то даже в С++17 много функций из STL не constexpr (в смысле, просто перед ними не приписан constexpr), и их не получается использовать на этапе компиляции.
                                                  • +1
                                                    Это не минус constexpr, это распространённое заблуждение на тему семантики constexpr и зачем оно нужно. constexpr [у функции] нужен не для того, чтобы гарантировать её вычисление во время компиляции, он нужен для того, чтобы компилятор на эту функцию в constexpr-контексте вообще посмотрел.
                                                    • +1
                                                      Вполне можно было вообще все функции считать constexpr по-умолчанию
                                                      Нельзя. По историческим причинам. Функции не описанные как constexpr компилятор, в большинстве случаев, в нужном контексте просто не видит (раздельная компиляция, всё такое). Так что constexpr таки нужен.

                                                      Но можно было бы всё inline-функции сделать constexpr — это правда…
                                                      • 0
                                                        Нельзя. По историческим причинам. Функции не описанные как constexpr компилятор, в большинстве случаев, в нужном контексте просто не видит (раздельная компиляция, всё такое).

                                                        Не могли бы вы объяснить по-подробнее? Почему прям нельзя?
                                                        Допустим, не видит компилятор тела функции из библиотеки — ну и ладно, в рантайме вызовем. В конце концов, constexpr не гарантирует выполнения на этапе компиляции.

                                                        Другое дело, что время компиляции от этого, скорее всего, сильно выросло бы.
                                                        • 0
                                                          В конце концов, constexpr не гарантирует выполнения на этапе компиляции.
                                                          В случае с описанием переменной (а также использования в качестве параметра типа, размера массива и в других местах) — гарантирует.

                                                          Не могли бы вы объяснить по-подробнее? Почему прям нельзя?
                                                          Потому что описать constexpr-функцию без тела — нельзя, а без constexpr-можно.

                                                          Но реально, конечно, это всё поблажки разработчикам компиляторов: constexpr-функции ведь приходится интерпретировать — а для этого, фактически, отдельный транслятор нужен… Вот и ограничивают их. Вначале ограничения были совсем драконовскими, но сейчас потихоньку гайки отпускают.
                                                    • 0
                                                      > Зачем же тогда указывать constexpr?

                                                      Если компилятор не сможет сделать constexpr, он об этом сообщит.
                                                      • +1
                                                        Если компилятор не сможет сделать constexpr, он об этом сообщит.
                                                        К сожалению всё не так. Это происходит в любом случае в месте подстановки. Посмотрите сами — вызов printf не машает функции отрабатывать в компайл-тайме. Если он не триггерится, конечно.

                                                        Ну так и нафига козе баян — в смысле нафига явно тут указывать, что функция constexpr? Если возможность её использовать всё равно зависит от конкретного значения?

                                                        P.S. Кстати за счёт constexpr любой коспилятор C++ — теперь где-то ещё и инетерпретатор. clang отрабатывает раз в 8 быстрее, чем gcc…
                                                    • +3

                                                      Отличная статья! Всё собрано в одном месте.

                                                      • +1
                                                        Здорово что они дополняют std простыми и полезными функциями. Но вот…
                                                        Свертка параметров шаблона (Fold expressions)
                                                        Сложность понимания кода все еще очень высока. Все еще нужно разбираться в нюансах, а не читать «на лету» код
                                                        • +5
                                                          Fold expressions, как раз уменьшают сложность работы с variadic templates, ведь код получается куда нагляднее чем с использованием рекурсии.
                                                        • +1
                                                          std::map<int, std::string> myMap;
                                                          ....
                                                          if (auto[it, ok] = myMap.insert({ 2, "hello" }); ok)
                                                          {
                                                            ....
                                                          }

                                                          Не могу понять зачем это надо в конкретном примере. Чем хуже сделать insert до if, а в if просто проверить ok?
                                                          • +2

                                                            Переменные it и ok живут в меньшем скопе

                                                            • +1
                                                              Это больше синтаксический сахар — не нужна лишняя строка. И да, выше указали более существенное преимущество про сужение scope
                                                            • +9
                                                              for (const auto &[key, value] : myMap)

                                                              Наконец-то не надо писать iter->first, iter->second!
                                                              • +1
                                                                Интересно. А настанет ли момент, когда из языка будут удалять ненужные элементы, а не добавлять новые?
                                                                • +2
                                                                  Начнется вой о совместимости со старым кодом.
                                                                  • +2

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

                                                                    • 0
                                                                      Верно! Для повторного использования кода вынужден писать на Delphi-7.
                                                                      • 0
                                                                        Во-первых не загнулся. Во-вторых совместимость вверх у него одна из самых лучших вообще существующих.
                                                                      • 0

                                                                        Просто не используйте ненужные элементы.

                                                                        • +6
                                                                          Другие-то их используют, в библиотеках, например. Скоро найти человека, который знает весь синтаксис С++ будет нереально. И это меня печалит.
                                                                          • +3

                                                                            Потому всё это напоминает мне PL/1, когда обычный программист не знал большей части языка, из-за чего возникали реальные проблемы с пониманием чужого кода написанного человеком, знающим другие части языка.

                                                                            • 0

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

                                                                            • 0
                                                                              Очень удобная позиция, но к сожалению не работает.
                                                                            • +1
                                                                              Так вроде std::auto_ptr удалили.
                                                                              • –4
                                                                                Когда случайно узнал — радовался неделю. Теперь жду когда же удалят iostream и все что с этим связано. Ну и половину нововведений со времен C++11.
                                                                                • 0

                                                                                  А с iostream-то что не так? Переделать его, возможно, и правда следует. Но удалять?..

                                                                                  • +7
                                                                                    У меня стойкое чувство, что iostream (и друзья) спроектированы по двум причинам: оправдать наличие в языке виртуальных классов (ромб смерти) и показать как здорово можно переопределять операторы (в данном случае << и >>). А для того чтобы показать на примере специализацию шаблонов специализирован класс std::vector. И т.д. и т.п.
                                                                                    • +4
                                                                                      std::vector<bool>
                                                                                      • +4

                                                                                        Эти классы взялись не на пустом месте, а были сделаны как абстракция понятия "поток данных". Во всех современных языках есть свои аналогичные абстракции:


                                                                                        Delphi 7 — TStream
                                                                                        Java — java.io.InputStream, java.io.OutputStream
                                                                                        C# — System.IO.Stream
                                                                                        Python — io.IOBase
                                                                                        Node.js — модуль stream


                                                                                        Поэтому без замены iostream на что-то более красивое убирать его из стандартной библиотеки нельзя.

                                                                                        • +6
                                                                                          Полностью согласен. Посмотрите на грамотные java.io.InputStream, java.io.OutputStream. А вот зачем было ввод и вывод мандить в std::ios? И зачем были нужны операторы << и >>. В реальных проектах они неудобны, сложно настраивать форматирование, невозможно делать локализацию и т.д. printf по сравнению с ними просто сказка.
                                                                                          • +4
                                                                                            и т.д. printf по сравнению с ними просто сказка.
                                                                                            Ровно до тех пор, пока не придется использовать printf в обобщенном коде. Или до тех пор, пока не придется использовать printf для вывода в определенный пользователем поток данных.

                                                                                            Для тех, кто исходит на известную субстанцию от iostreams, уже давно есть fmtlib. Которая, среди прочего, позволяет работать с пользовательскими типами, для которых определены операторы сдвига именно в std::ostream.
                                                                                          • +3

                                                                                            Я думаю, имеется ввиду выкинуть iostreams на мороз именно в текущем виде. Просто ради примера, в Rust абстракции байтовых потоков ввода-вывода требуют реализации по одной функции на чтение и запись, соответственно. Это в самой базовой форме. В С++ тщательно ковыряться с буфером потока и, возможно, самим классом потока.

                                                                                      • 0
                                                                                        а конкретней, какие именно новые фичи нужно удалить?
                                                                                        • –1
                                                                                          Ну списка я не веду, дабы не расстраиваться. Для примера Вам новая инициализация через { и }.
                                                                                          • +3
                                                                                            А чем она хуже того, что было ранее, кроме того что нужно сесть и разобраться в том, как она работает? C++ — expert friendly. Одна из идей языка — не жертвовать гибкостью в пользу простоты изучения. Есть языки с противоположной парадигмой — golang, например.
                                                                                            Конечно, в С++ есть места, где сложность получается избыточной и не несёт полезной нагрузки — с ними пытаются бороться.
                                                                                            Я не говорю, что golang — хуже, просто это другой язык с другой установкой, идеей и целями. Не нужно все языки под golang причёсывать.
                                                                                            • 0
                                                                                              Насчет { } немного долгое объяснение. Когда-то очень давно, когда в C++ вводились классы их синтаксически приравняли к структурам. Но в моей реальной практике (и не только в моей) слово class используют, когда нужен полноценный объект, а слово struct, когда нужны только переменные члены класса (и они все открытые). В struct практически не пишутся функции, иногда пишется конструктор, еще реже деструктор. И вот именно дефолтного конструктора по всем членам структуры мне всегда не хватало. Скобки { }
                                                                                              решают это проблему, но заодно вводят путаницу в вызовы конструкторов. По мне, так правильней было бы разделить назначение struct и class. И как минимум сделать дефолтный конструктор для struct по списку членов.
                                                                                              • 0
                                                                                                Ну то есть по Вашему мнению тот факт, что {} инициализация могла бы быть сделана ещё лучше означает, что её нужно совсем удалить? Да и Ваш вариант обладает недостатком нарушения совместимости с C++98, так что его превосходство крайне спорно. Новых еще лучших несовместимых языков хватает — D, Rust, golang,… У C++ другие цели, при этом преимуществ более новых языков никто не отрицает.
                                                                                                • 0
                                                                                                  Два крайних подхода. 1. Если что-то можно сделать в языке без ввода новых конструкций, то новую конструкцию не вводят. 2. Вводить новую конструкцию по любому мелочному поводу.
                                                                                                  Истина где-то посередине. Но середина у всех разная. У меня она ближе к пункту 1.
                                                                                                  Про разделения struct и class я говорил (увы не уточнил) в контексте, как надо было сделать, когда вводились классы. Сейчас, понятно, это сделать уже нельзя.
                                                                                    • 0
                                                                                      Особенно понравился [[maybe_unused]]! Интересно, а скоро будет [[maybe_wrong]]?
                                                                                      • 0
                                                                                        Думаю будет полезно определение [[maybe_error]] — если функция не используется, то может содержать ошибки (не компилироваться). Извините за черный юмор.
                                                                                        • 0
                                                                                          Извините за черный юмор

                                                                                          Увы! От «плюсов» иногда появляются черные мысли…
                                                                                        • +5
                                                                                          [[maybe_unused]] полезен при условной компиляции.
                                                                                          • +3
                                                                                            Совершенно неуместная ирония. Вы хорошо знакомы с C++? Встречали паттерн (void)param2;? Это ему более удобная замена. Кроме условной компиляции такое бывает необходимо при перегрузке виртуальных функций, мб ещё где…
                                                                                            • 0
                                                                                              Да. Увы. Знаком. Перманентно перевожу с С++ на Delphi-7 (а еще CUDA — там проще без Delphi), всякие паттерны дополнительно затрудняют перевод. С++ может и великий, но не единственный язык, поэтому ИМХО стоит больше думать о сосуществовании с другими языками.
                                                                                              • +3
                                                                                                И как это связанно с [[maybe_unused]]? Что в нём плохого? Это же просто подсказка программисту от компилятора типа тех которые выдает статический анализатор.
                                                                                                C++ и так сделал всё необходимое для сосуществования — обертки над C++ доступны практически во всех языках.
                                                                                                • 0
                                                                                                  Я не говорил, что в [[maybe_unused]] что-то плохое. И уж точно не собирался устраивать холивар «какой язык лучше», я только хотел сказать, что ИМХО слишком много стало архитектурных излишеств, как было сказано выше:

                                                                                                  Скоро найти человека, который знает весь синтаксис С++ будет нереально. И это меня печалит.
                                                                                                  • 0
                                                                                                    А почему именно эта конструкция излишняя? При чем тут архитектура? что значит «излишняя» для вас? Так-то до ассемблера можно дойти. И даже дальше.
                                                                                                    • 0
                                                                                                      На мой взгляд, эта конструкция одна из наиболее выразительных, где избыточность выражена через maybe. Архитектура тут при том, что было постановление.

                                                                                                      что значит «излишняя» для вас?

                                                                                                      Думаю, очевидно, что для меня как переводчика это лишняя заморочка.

                                                                                                      Так-то до ассемблера можно дойти.


                                                                                                      А что Вы имеете против ассемблера?

                                                                                                      Вспомнил, нпр., такое утверждение:

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

                                                                                                      Далее:
                                                                                                      И даже дальше.

                                                                                                      А куда дальше ассемблера?

                                                                                                      • +1
                                                                                                        Какое это имеет отношение к вопросу «почему именно эта конструкция излишняя?».
                                                                                                        конструкция одна из наиболее выразительных, где избыточность выражена через maybe
                                                                                                        то есть она все таки не излишняя?
                                                                                                        А настанет ли момент, когда из языка будут удалять ненужные элементы

                                                                                                        Особенно понравился [[maybe_unused]]!
                                                                                                        … слишком много стало архитектурных излишеств

                                                                                                        то есть, очевидно, слово «понравился» имело ироничный характер.
                                                                                                        для меня как переводчика это лишняя заморочка.
                                                                                                        Во-первых даже для «переводчиков» эта заморочка совершенно не лишняя т.к. показывает намерение о том, что эта переменная на самом деле, вероятно, не нужна и её можно не переносить. Во-вторых, мне кажется, очевидно, что интересы переводчиков с С++ при разработке стандарта имеют крайне низкий приоритет. Гораздо важнее удобство C++ разработки.
                                                                                                        А что Вы имеете против ассемблера?
                                                                                                        Вы специально из контекста выдёргиваете? Там речь шла о том, что ассемблер — плохой C++ (и наоборот, но речь не об этом). Соответсвенно, «излишества» — понятие относительное. И снова возвращаемся к вопросу, почему Вы считаете, что конкретно "[[maybe_unused]]" — излишество, причём самое явное в новом стандарте.

                                                                                                        А приводить в качестве аргумента об архитектуре ЯП постановление ЦК КПСС по архитектуре зданий от 1955 года — это вообще какая-то самодискредитация. Во-первых не та предметная область. Во-вторых в строительстве нет единого понимания о том, что такое хорошо, но постановления ЦК КПСС тут точно не авторитет, также как и по отношению к генетике и кибернетике. И в третьих последствия этого решения всем нам печально известны в виде страшных как смертный грех жилых районов в городах СНГ, которые не критиковал только ленивый. Примеров того, как от этой «архитектуры» старались и стараются избавиться, везде, где представляется такая возможность — валом.
                                                                                                        • +1
                                                                                                          А приводить в качестве аргумента об архитектуре ЯП постановление ЦК КПСС по архитектуре зданий от 1955 года


                                                                                                          Я сказал "архитектура", а не «архитектура ЯП»! И не приводил в качестве аргумента, а только в качестве сравнения, метафоры.

                                                                                                          очевидно, что интересы переводчиков с С++ при разработке стандарта имеют крайне низкий приоритет.


                                                                                                          Именно это я и говорил:

                                                                                                          стоит больше думать о сосуществовании с другими языками


                                                                                                          И возможно ли доказать, что удобство C++ разработки повысилось?:

                                                                                                          Гораздо важнее удобство C++ разработки.
                                                                                          • 0
                                                                                            Так два года назад начали:
                                                                                            «Within C++ is a smaller, simpler, safer language struggling to get out.» — Bjarne Stroustrup
                                                                                            • 0
                                                                                              «Within C++ is a smaller, simpler, safer language losing struggle to get out.»

                                                                                              Fixed

                                                                                              • 0
                                                                                                Ещё рано судить, мне кажется. Я готов дать Core Guidelines шанс.
                                                                                                • –1

                                                                                                  Увы, я потихоньку разувериваюсь. Некоторые хронические болячки либо не решаются, либо решаются с адскими задержками. Зато накидывают всякой ерунды — вроде зета-функции Римана. Вот самое место в стандарте! Проблема же миграции на другие языки часто в том, что С++ несовместим ни с кем кроме С++ — причём часто только своим диалектом.

                                                                                                  • 0
                                                                                                    С++ несовместим ни с кем кроме С++

                                                                                                    C++ совместим (хотя бы частично) с С, а это в эмбеддеде уже огромный плюс. Вот кто точно почти не совместим ни с кем, кроме себя — это стандартный .NET, который представляет собой вещь в себе и приносящий дикие боли при попытке подружить что-либо с нативной библиотекой (писать кучу [DllImport] та еще романтика, буэ)

                                                                                                    • 0
                                                                                                      Для сложных случаев в .NET существуют аж две альтернативы DllImport — COM и C++/CLI
                                                                                                      • +2

                                                                                                        И обе работают только в пределах MS Windows?


                                                                                                        Но дотнет еще ничего, я боюсь подумать про Node.js и прочие "новомодные" фреймворки.

                                                                                                        • 0
                                                                                                          Да нет, вроде бы и в линуксе есть… Хотя сам я не проверял как оно там работает.
                                                                                                          • 0
                                                                                                            Но дотнет еще ничего, я боюсь подумать про Node.js и прочие «новомодные» фреймворки.

                                                                                                            Обёртки писать, как ещё. Для Python много всего понаписано. Для node.js тоже статьи попадались — видимо как-то можно.