Рисунок 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. Пожалуйста, ознакомьтесь со списком.

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


  1. NYMEZIDE
    13.10.2017 10:28
    +8

    скоро::программировать<'на'> &зыке << !С++ ^будут* << _так?;


    1. dmpas
      13.10.2017 10:34

      это ведь не случайно, что у вас написано кириллицей? Импортозамещение и все дела?


      1. qw1
        13.10.2017 20:36

        Ну, C# позволяет писать идентификаторы любыми символами, которые в стандарте unicode определены как Letter, почему бы в C++ не принять эту фичу.


        1. netch80
          13.10.2017 22:01

          Тут возникает ряд существенных граблей.
          Например, обязан ли язык различать A (U+00C1) и A? (U+0041 U+0301)? Python различает, но приведение всех идентификаторов к NKFC может быть дороговато (в компилятор втягивать что-то размера ICU, причём на этапе, где лексер и так отрабатывает много специфики языка — см. например Clang — где есть лексический парсинг обычного кода, строк препроцессора, кода внутри блока `#if 0`, и т.п.) А вот Go ничего не делает с этим — и можно получить злобные проблемы от внешне одинаковых идентификаторов в разных местах, так, что редактор этого не покажет.
          А когда апдейтится стандарт Unicode — менять таблицы вслед?
          Требовать ли от внешних средств типа линкера поддержки таких идентификаторов, и что делать, если не поддерживает?

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

          Ведь даже наличие ASCII, считаем, стало обязательным только с C++17 — я имею в виду отказ от триграфов, которые применялись для возможности написания на C++ в странных местах типа zSeries с локализованными вариантами EBCDIC…


          1. khim
            14.10.2017 01:26

            В случае с 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.

            И компиляторы, хотя и не все, давно позволяют называет идентификаторы по русски. Это вопрос к программистам, а не к стандарту сегодня, на самом деле.


        1. khim
          14.10.2017 01:24

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


    1. SvyatoslavMC
      13.10.2017 10:45
      +1

      1С++17?


  1. Dzen1
    13.10.2017 11:41

    Спасибо за статью.


  1. ababo
    13.10.2017 11:50

    Чувствуется влияние Golang и Rust.


    1. DaylightIsBurning
      13.10.2017 16:32
      +5

      <sarcasm>особенно golang</sarcasm>


      1. ababo
        13.10.2017 18:15
        -2

        Только не включайте больше ваш сарказм, пожалуйста. А просто перечитайте про инициализаторы в if и switch.


      1. netch80
        13.10.2017 19:20
        -1

        Возможность «инициализатор в if и switch» — это как раз если не впервые в таком виде появилось в Go, то по крайней мере там стало хорошо известно — и доказало свою полезность.
        Или Вы знаете другой язык из популярных, где это было раньше?


        1. bfDeveloper
          13.10.2017 19:28
          +3

          Инициализаторы в if в C++ были очень давно. Не хватало возможности определить условие, отличное от приведения проинициализированной переменной к bool.


          1. netch80
            13.10.2017 19:33
            -1

            Ну то есть они были практически бесполезны.
            А вот именно в такой удобной форме (и с блоком на весь if/switch) — образцом послужил Go.


            1. qw1
              13.10.2017 20:38

              С указателями получалось очень даже красиво:

              if (foo* p = findFoo(id)) { p->bar(); }


            1. khim
              14.10.2017 01:28

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

              Когда эта фича в C++ появилась — ни rust'а, ни go даже в проекте не было.


        1. DaylightIsBurning
          13.10.2017 21:14

          if (y = f(x), y > x) {
              ... // statements involving x and y
          }
          


          1. DaylightIsBurning
            13.10.2017 21:18

            новый синтаксис — просто развитие идеи.


          1. netch80
            13.10.2017 21:28

            Упускаете главный момент.
            Новая форма аналогична созданию блока, в котором определены новые переменные, а после этого вложен собственно 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. DaylightIsBurning
              13.10.2017 21:38

              Я с этим и не спорю. С моей точки зрения это никак не связано с go, все совпадения с go случайны :).


              1. netch80
                13.10.2017 21:44

                А Вы где-то ещё видели именно такой же синтаксис — две части через точку с запятой, первая необязательна, но должна что-то инициализировать? Я — только в одном источнике, и его тут уже назвали.
                Был бы другой образец — взяли бы его, потому что ничто не мешало, например, создать отдельное ключевое слово и блок за ним (что было бы как-то более понятно при отсутствии такого образца), или сделать другое построение блока (хм, а почему if и switch, но не while?), перетащить GCC вариант ({...}) с уточнением блочного контекста, и т.п.


                1. DaylightIsBurning
                  13.10.2017 21:55
                  +1

                  ну, например, тут:

                  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.


                  1. netch80
                    13.10.2017 22:12

                    > распространение этого синтаксиса и на 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. DaylightIsBurning
                  13.10.2017 22:20

                  А Вы где-то ещё видели именно такой же синтаксис — две части через точку с запятой,
                  А какое это имеет отношение к делу? После не значит в следствии. Мысль о том, что if не хватает инициализации посещала меня ещё до появления golang. Да и сами авторы пропозала тоже мотивируют внутренними потребностями языка.


  1. eao197
    13.10.2017 11:57

    Вот от этого фрагмента прям какой-то теплой ламповой сишечкой повеяло:

    auto res1 = std::to_chars(arr, arr + 128, 3.14f);
    Может имело бы смысл его записать в духе современного C++?
    auto res1 = std::to_chars(std::begin(arr), std::end(arr), 3.14f);
    Как-то понадежнее будет.


    1. EgorBredikhin Автор
      13.10.2017 12:26

      Да, согласен, спасибо. Подправил в статье.


      1. a1ien_n3t
        13.10.2017 12:49

        Очень странно что to_chars возвражает структуру, а не std::pair
        Тогда бы и тут можно было писать что-то типа
        auto [ptr, error] = std::to_chars(...)


        1. EgorBredikhin Автор
          13.10.2017 12:54
          +3

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


    1. TargetSan
      13.10.2017 12:48

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


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

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


      1. EgorBredikhin Автор
        13.10.2017 13:01
        +1

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


        1. TargetSan
          13.10.2017 13:05

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


      1. eao197
        13.10.2017 13:04

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

        Другое дело, что к to_chars_result::ptr можно получить доступ даже если в to_chars_result::ec находится код ошибки… Вот это не есть хорошо. Какой-нибудь std::variant<char*, std::errc> был бы, наверное, уместнее. Но, вероятно, с точки зрения эффективности to_chars_result обходится дешевле.


        1. MooNDeaR
          13.10.2017 13:42

          Вот тут меня тож переклинило. Зачем спрашивается std::optional?


          1. eao197
            13.10.2017 13:54

            std::optional не поможет вернуть код ошибки в случае неудачи. А вот аналог Rust-овского Result-а в стандартной библиотеке бы не помешал.


            1. 0xd34df00d
              13.10.2017 18:56

              Поверх std::variant можно сделать Result/Either. Даже с монадической структурой (только вместо >>= придётся писать >>, так как >>= правоассоциативен).

              Но было бы приятно, если бы это было в стандартной библиотеке, да.


              1. JegernOUTT
                13.10.2017 23:32

                Есть интересный proposal как раз на эту тему http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0650r0.pdf
                Ждём, верим :)


  1. xFFFF
    13.10.2017 11:58

    Когда уже откажутся от header-файлов?


    1. kosmonaFFFt
      13.10.2017 12:13

      Когда завезут модульную систему + лет 10 (а может и больше) на избавление от легаси.


    1. Idot
      13.10.2017 12:33

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


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


      1. ZakharS
        13.10.2017 12:36

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


        1. TargetSan
          13.10.2017 12:54

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


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


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


      1. xFFFF
        13.10.2017 13:18
        +1

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


        1. yarric
          13.10.2017 13:48
          -1

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


          1. mayorovp
            13.10.2017 14:16

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


            1. Petrenuk
              14.10.2017 02:07

              using никак не сравнится по гибкости и удобству с RAII в С++


              1. qw1
                14.10.2017 09:02

                using это просто синтаксический сахар над finally { .Dispose(); } и ничего более.


                1. Petrenuk
                  14.10.2017 15:02

                  Да, я в курсе. Был бы RAII также любим если бы нужно было писать obj.~Object(); каждый раз?)


                  1. qw1
                    14.10.2017 15:18

                    То есть, разрушение C++ объектов при выходе из скоупа — тоже сахар?


                    1. Petrenuk
                      14.10.2017 15:20

                      Получается так :D


            1. qw1
              14.10.2017 09:01
              +1

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


              1. Petrenuk
                14.10.2017 15:02

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


          1. F0iL
            13.10.2017 15:21

            ru.wikipedia.org/wiki/Ngen
            По сути дела, нативная компиляция.


            1. yarric
              14.10.2017 11:32

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


              1. F0iL
                15.10.2017 01:10

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

                Теоретически, на некоторых задачах даже обгонять может (по аналогии с оптимизирующими JIT-компиляторами — встречал подобные тесты для JVM, например), т.к. бинарь компилируется непосредственно под имеющуюся известную процессорную архитектуру/поколение, следовательно, можно сразу очень эффективно оптимизировать код.


                1. FoxCanFly
                  15.10.2017 14:36

                  Все компиляторы C++ умеют компилировать под имеющуюся известную процессорную архитектуру/поколение


                  1. qw1
                    15.10.2017 15:07

                    В дистрибутивах, где всё собирается из исходников под свою машину, традиционные компиляторы всегда лучше jit.


                  1. F0iL
                    15.10.2017 18:18

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


    1. Hazactam
      13.10.2017 18:50

      Когда уже откажутся от header-файлов?

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

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


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

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


      1. alan008
        13.10.2017 20:30

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


  1. ZakharS
    13.10.2017 12:30

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


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


    1. ZakharS
      13.10.2017 12:41

      Постоянное дежавю с питоном. Похоже, в комитет проникли питонисты

      auto[iter, ok] = mySet.insert(42);
      Python: a,b=1,2


      1. 0xd34df00d
        13.10.2017 17:21
        +2

        Скорее с хаскелем. Структуру вы так в питоне не разложите.


        1. splav_asv
          13.10.2017 19:25

          Структура в Python это namedtuple — очень даже разложится.


      1. netch80
        13.10.2017 21:39

        Ну вообще-то концепция, по которой у функции всегда предполагалось только одно значение-результат, выглядит устаревшей. Она была хороша в период ранней «математизации» понятия функции (особенно по сравнению с subroutine в Fortran, где функции обязаны были быть «чистыми»), но сейчас нет смысла добровольно вжиматься в прокрустово ложе. Тем более что примеров, когда реально передаётся несколько значений, но все кроме одного идут косвенными каналами — полно в любом системном API.

        А с чем сравнивать возврат нескольких значений — с Python, Go, Swift, Erlang, Haskell, чем-то ещё — вопрос персонального опыта. В данном случае второй ok это ближе к тому, что я видел по Go. Был бы он первым — был бы стиль Erlang :)


        1. Free_ze
          16.10.2017 14:10

          Чем это принципиально отличается от ссылочных out-параметров?


          1. netch80
            16.10.2017 20:41

            Чтобы ответить на этот вопрос, мне нужно знать уровень Вашей «принципиальности» подхода к этой разнице. Но есть два основных аспекта:
            1. Формальный — что это именно части полного результата функции и соответственно не требуют рисования промежуточных переменных.
            2. Практический — что в точно такой же манере, как современные ABI предпочитают передавать K первых параметров через регистры, чтобы гонять через RAM — результаты тоже можно передавать через регистры и не заниматься косвенным доступом.
            Ну а насколько Вам это будет принципиально — не могу предсказать.


    1. Amomum
      13.10.2017 16:40

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


      1. 0xd34df00d
        13.10.2017 20:19

        Это не минус constexpr, это распространённое заблуждение на тему семантики constexpr и зачем оно нужно. constexpr [у функции] нужен не для того, чтобы гарантировать её вычисление во время компиляции, он нужен для того, чтобы компилятор на эту функцию в constexpr-контексте вообще посмотрел.


      1. khim
        14.10.2017 01:34

        Вполне можно было вообще все функции считать constexpr по-умолчанию
        Нельзя. По историческим причинам. Функции не описанные как constexpr компилятор, в большинстве случаев, в нужном контексте просто не видит (раздельная компиляция, всё такое). Так что constexpr таки нужен.

        Но можно было бы всё inline-функции сделать constexpr — это правда…


        1. Amomum
          14.10.2017 01:55

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

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

          Другое дело, что время компиляции от этого, скорее всего, сильно выросло бы.


          1. khim
            14.10.2017 02:36

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

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

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


    1. andy_p
      13.10.2017 16:49

      > Зачем же тогда указывать constexpr?

      Если компилятор не сможет сделать constexpr, он об этом сообщит.


      1. khim
        14.10.2017 01:39

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

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

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


  1. sergio_nsk
    13.10.2017 12:42

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


  1. Gizmich
    13.10.2017 12:42

    Здорово что они дополняют std простыми и полезными функциями. Но вот…

    Свертка параметров шаблона (Fold expressions)
    Сложность понимания кода все еще очень высока. Все еще нужно разбираться в нюансах, а не читать «на лету» код


    1. EgorBredikhin Автор
      13.10.2017 12:42
      +3

      Fold expressions, как раз уменьшают сложность работы с variadic templates, ведь код получается куда нагляднее чем с использованием рекурсии.


  1. ooki2day
    13.10.2017 12:46

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

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


    1. JegernOUTT
      13.10.2017 12:48

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


    1. ZakharS
      13.10.2017 12:48

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


  1. ZakharS
    13.10.2017 12:53

    for (const auto &[key, value] : myMap)

    Наконец-то не надо писать iter->first, iter->second!


  1. Mike255
    13.10.2017 12:54

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


    1. xFFFF
      13.10.2017 13:33

      Начнется вой о совместимости со старым кодом.


    1. Idot
      13.10.2017 13:37

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


      1. third112
        13.10.2017 15:44

        Верно! Для повторного использования кода вынужден писать на Delphi-7.


      1. Hazactam
        13.10.2017 18:53

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


    1. yarric
      13.10.2017 13:50

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


      1. Mike255
        13.10.2017 15:37
        +1

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


        1. Idot
          13.10.2017 18:23
          +2

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


        1. yarric
          14.10.2017 14:11

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


      1. Fedcomp
        13.10.2017 17:06

        Очень удобная позиция, но к сожалению не работает.


    1. alexeykuzmin0
      13.10.2017 15:39

      Так вроде std::auto_ptr удалили.


      1. Mike255
        13.10.2017 15:42
        -2

        Когда случайно узнал — радовался неделю. Теперь жду когда же удалят iostream и все что с этим связано. Ну и половину нововведений со времен C++11.


        1. mayorovp
          13.10.2017 15:55

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


          1. Mike255
            13.10.2017 15:59

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


            1. Mike255
              13.10.2017 16:04

              std::vector<bool>


            1. mayorovp
              13.10.2017 16:18
              +2

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


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


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


              1. Mike255
                13.10.2017 16:29
                +1

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


                1. eao197
                  13.10.2017 17:20
                  +2

                  и т.д. printf по сравнению с ними просто сказка.
                  Ровно до тех пор, пока не придется использовать printf в обобщенном коде. Или до тех пор, пока не придется использовать printf для вывода в определенный пользователем поток данных.

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


              1. TargetSan
                13.10.2017 16:42
                +1

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


        1. DaylightIsBurning
          13.10.2017 17:00

          а конкретней, какие именно новые фичи нужно удалить?


          1. Mike255
            13.10.2017 17:10

            Ну списка я не веду, дабы не расстраиваться. Для примера Вам новая инициализация через { и }.


            1. DaylightIsBurning
              13.10.2017 17:17
              +2

              А чем она хуже того, что было ранее, кроме того что нужно сесть и разобраться в том, как она работает? C++ — expert friendly. Одна из идей языка — не жертвовать гибкостью в пользу простоты изучения. Есть языки с противоположной парадигмой — golang, например.
              Конечно, в С++ есть места, где сложность получается избыточной и не несёт полезной нагрузки — с ними пытаются бороться.
              Я не говорю, что golang — хуже, просто это другой язык с другой установкой, идеей и целями. Не нужно все языки под golang причёсывать.


              1. Mike255
                13.10.2017 21:28

                Насчет { } немного долгое объяснение. Когда-то очень давно, когда в C++ вводились классы их синтаксически приравняли к структурам. Но в моей реальной практике (и не только в моей) слово class используют, когда нужен полноценный объект, а слово struct, когда нужны только переменные члены класса (и они все открытые). В struct практически не пишутся функции, иногда пишется конструктор, еще реже деструктор. И вот именно дефолтного конструктора по всем членам структуры мне всегда не хватало. Скобки { }
                решают это проблему, но заодно вводят путаницу в вызовы конструкторов. По мне, так правильней было бы разделить назначение struct и class. И как минимум сделать дефолтный конструктор для struct по списку членов.


                1. DaylightIsBurning
                  13.10.2017 21:46

                  Ну то есть по Вашему мнению тот факт, что {} инициализация могла бы быть сделана ещё лучше означает, что её нужно совсем удалить? Да и Ваш вариант обладает недостатком нарушения совместимости с C++98, так что его превосходство крайне спорно. Новых еще лучших несовместимых языков хватает — D, Rust, golang,… У C++ другие цели, при этом преимуществ более новых языков никто не отрицает.


                  1. Mike255
                    13.10.2017 21:54

                    Два крайних подхода. 1. Если что-то можно сделать в языке без ввода новых конструкций, то новую конструкцию не вводят. 2. Вводить новую конструкцию по любому мелочному поводу.
                    Истина где-то посередине. Но середина у всех разная. У меня она ближе к пункту 1.
                    Про разделения struct и class я говорил (увы не уточнил) в контексте, как надо было сделать, когда вводились классы. Сейчас, понятно, это сделать уже нельзя.


    1. third112
      13.10.2017 15:49
      +1

      Особенно понравился [[maybe_unused]]! Интересно, а скоро будет [[maybe_wrong]]?


      1. Mike255
        13.10.2017 15:52
        +1

        Думаю будет полезно определение [[maybe_error]] — если функция не используется, то может содержать ошибки (не компилироваться). Извините за черный юмор.


        1. third112
          13.10.2017 16:00

          Извините за черный юмор

          Увы! От «плюсов» иногда появляются черные мысли…


      1. BiTHacK
        13.10.2017 15:57

        [[maybe_unused]] полезен при условной компиляции.


      1. DaylightIsBurning
        13.10.2017 17:11

        Совершенно неуместная ирония. Вы хорошо знакомы с C++? Встречали паттерн (void)param2;? Это ему более удобная замена. Кроме условной компиляции такое бывает необходимо при перегрузке виртуальных функций, мб ещё где…


        1. third112
          13.10.2017 22:11

          Да. Увы. Знаком. Перманентно перевожу с С++ на Delphi-7 (а еще CUDA — там проще без Delphi), всякие паттерны дополнительно затрудняют перевод. С++ может и великий, но не единственный язык, поэтому ИМХО стоит больше думать о сосуществовании с другими языками.


          1. DaylightIsBurning
            13.10.2017 22:15

            И как это связанно с [[maybe_unused]]? Что в нём плохого? Это же просто подсказка программисту от компилятора типа тех которые выдает статический анализатор.
            C++ и так сделал всё необходимое для сосуществования — обертки над C++ доступны практически во всех языках.


            1. third112
              13.10.2017 23:02

              Я не говорил, что в [[maybe_unused]] что-то плохое. И уж точно не собирался устраивать холивар «какой язык лучше», я только хотел сказать, что ИМХО слишком много стало архитектурных излишеств, как было сказано выше:

              Скоро найти человека, который знает весь синтаксис С++ будет нереально. И это меня печалит.


              1. DaylightIsBurning
                13.10.2017 23:11

                А почему именно эта конструкция излишняя? При чем тут архитектура? что значит «излишняя» для вас? Так-то до ассемблера можно дойти. И даже дальше.


                1. third112
                  13.10.2017 23:44

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

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

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

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


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

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

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

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

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


                  1. DaylightIsBurning
                    14.10.2017 00:14

                    Какое это имеет отношение к вопросу «почему именно эта конструкция излишняя?».

                    конструкция одна из наиболее выразительных, где избыточность выражена через maybe
                    то есть она все таки не излишняя?
                    А настанет ли момент, когда из языка будут удалять ненужные элементы

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

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

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


                    1. third112
                      14.10.2017 01:31

                      А приводить в качестве аргумента об архитектуре ЯП постановление ЦК КПСС по архитектуре зданий от 1955 года


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

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


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

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


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

                      Гораздо важнее удобство C++ разработки.


    1. DaylightIsBurning
      13.10.2017 16:56

      Так два года назад начали:

      «Within C++ is a smaller, simpler, safer language struggling to get out.» — Bjarne Stroustrup


      1. TargetSan
        13.10.2017 16:59

        «Within C++ is a smaller, simpler, safer language losing struggle to get out.»

        Fixed


        1. DaylightIsBurning
          13.10.2017 17:06

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


          1. TargetSan
            13.10.2017 17:19

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


            1. Dima_Sharihin
              14.10.2017 12:31

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

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


              1. mayorovp
                14.10.2017 14:17
                +1

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


                1. Dima_Sharihin
                  14.10.2017 15:15
                  +1

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


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


                  1. mayorovp
                    14.10.2017 16:27

                    Да нет, вроде бы и в линуксе есть… Хотя сам я не проверял как оно там работает.


                  1. splav_asv
                    14.10.2017 21:07

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

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


            1. encyclopedist
              15.10.2017 00:45

              Эти функции были добавлены в C++ для совместимости с C, где они появились в C99.


      1. yarric
        14.10.2017 14:13

        Интересно, есть ли утилита или файл конфигурации для линтера, чтобы обеспечить строгое следование этим гайдам?


  1. ZakharS
    13.10.2017 13:58

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

    By pretending that parallelisation is simple – it has an enormous potential for unsuspecting developer trying to use it – and getting the whole project badly burned

    Детали здесь


    1. DaylightIsBurning
      13.10.2017 17:02
      +1

      Так C++ всегда был expert friendly, новичкам ничего не обещали :)


      1. hdfan2
        14.10.2017 15:32

        С++ — очень дружелюбный язык. Проблема в том, что он сам выбирает, с кем ему дружить.


  1. ZakharS
    13.10.2017 14:05

    Большое спасибо за прекрасный обзор и, главное, ясные примеры! Вы решили последовать совету Страуструпа на CppCon 2017 :) Он сказал, что самое главное — это не фичи, а примеры их правильного применения.

    Вообще, новый стандарт никак не минорный, каким был С++14. И хотя многое из списка выглядит как синтаксический сахар — все очень востребовано и поможет заметно улучшить читаемость и не писать лишний код. В который раз убеждаюсь, что в комитете сидят очень адекватные люди. А всеми ожидаемые Networking и Modules задерживают не просто так — там слишком много всего надо учесть, чтобы потом не переделывать и не ломать совместимость.


  1. Xandrmoro
    13.10.2017 14:36

    С одной стороны, много клёвых удобных фич, с другой — все дальше и дальше write-only…


  1. Petrenuk
    13.10.2017 15:42

    Чёрт возьми, а я и не знал, что столько всего полезного всё-таки попало в стандарт! Декомпозиция через auto, свёртки для variadic templates — это очень прикольно. Надо начинать использовать)


  1. Viacheslav01
    13.10.2017 17:29
    +3

    Во что превратился С++? Есть люди не из суперов заседающих в комитете, кто знает все детали языка?


    1. DaylightIsBurning
      13.10.2017 22:07

      Это сродни жалоб на то, что не все знают русский на уровне профессора фил-фака МГУ, а профессор МГУ не понимает профессиональных жаргонов.
      Вообще эта проблема сущесествует уже давно, шаблонная магия неспроста называется магией. К счастью, она обычно изолированна в тех местах, куда большинство разработчиков почти не заглядывает.
      Нельзя создать тривиальный для понимания и при этом универсальный (гибкий) язык, коим старается быть C++. К этому идеалу можно стремится, и C++-next + Core Guidelines как раз в этом направлении и двигаются.


      1. third112
        14.10.2017 01:42

        универсальный (гибкий) язык


        универсальный = гибкий?

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


        1. qw1
          14.10.2017 09:10
          +1

          Нет такого языка, как «Ассемблер», а есть языки «Ассемблер ARMv8», «Ассемблер x86» последний ещё и с разными синтаксисами (Intel, AT&T) и диалектами (i386, i686, SSE2).

          И который из этих ассемблеров универсальный?


          1. third112
            14.10.2017 14:32

            Каждый универсальный для своей платформы.


            1. qw1
              14.10.2017 14:54

              Почему тогда нельзя сказать: каждый язык программирования универсальный… для своего класса задач. Не теряется ли суть понятия «универсальный»?


              1. third112
                14.10.2017 15:05

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


                1. qw1
                  14.10.2017 15:16

                  Так и ассемблер работает на одной платформе, а не на широком круге.


                  1. third112
                    14.10.2017 15:31

                    Я не про платформы, а про задачи. Язык запросов SQL, нпр., реализован на многих платформах, но не является универсальным.


                    1. qw1
                      14.10.2017 16:36

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


                      1. third112
                        14.10.2017 17:23

                        Платформу выбирают под задачу.
                        Кто выбирает? Я не выбираю. Сейчас все задачи решаю на Intel Core i7, а было время, когда решал на ЕС-1022. А если серьезно, то существуют эмуляторы (см. вики) — можно любой ассемблер на любой достаточно мощной машине воспроизвести. Универсальность — свойство, заложенное в языке, а не ассортимент реализаций. Нпр., нет Дельфи-7 для CUDA, но возможно реализовать.


                        1. qw1
                          14.10.2017 20:03

                          Кто выбирает? Я не выбираю
                          Значит, узкий спектр задач. Например, игру типа Pokemon GO логично делать на мобильных платформах, а не на Core-i7.
                          А если серьезно, то существуют эмуляторы (см. вики) — можно любой ассемблер на любой достаточно мощной машине воспроизвести
                          А что толку, если платформа (в более широком смысле, чем аппаратная) вам не позволит писать на ассемблере. Например, если требуется сделать клиентскую логику на веб-странице.


                          1. third112
                            14.10.2017 20:20

                            Значит, узкий спектр задач. Например, игру типа Pokemon GO логично делать на мобильных платформах, а не на Core-i7.
                            ИМХО Pokemon GO очень специфичная задача. Но вроде QEMU поможет сделать ее на Core-i7.


                        1. qw1
                          14.10.2017 20:10

                          Универсальность — свойство, заложенное в языке, а не ассортимент реализаций
                          Ассемблер для ЕС-1022 и для Core-i7 — один язык, или разные?


                          1. third112
                            14.10.2017 20:22

                            разные.


                            1. qw1
                              15.10.2017 12:51

                              И оба — универсальные?


                              1. third112
                                15.10.2017 13:45

                                Да.


                                1. qw1
                                  15.10.2017 15:04
                                  +1

                                  Я вас понял, особенно после замечания про QEMU. Универсальность вы рассматриваете с математической, а не практической стороны. Ну, как машину Тьюринга.

                                  В таком случае, ассемблеры не являются чем-то особенным, любой полный по Тьюрингу язык можно считать универсальным. Тот же javascript — пишем на нём эмулятор x86, ставим windows, qemu, запускаем эмулятор андроида — задача выполнена.


                                  1. third112
                                    15.10.2017 16:18

                                    Согласен. В начале ветки я спросил:

                                    универсальный = гибкий?

                                    Изменю утверждение:

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


                                    1. qw1
                                      15.10.2017 16:37

                                      Я понимаю слово «гибкий» как «адаптирующийся к изменениям».

                                      То есть язык, который с одной стороны развивается в соответствии с веяниями программисткой моды (как c#), с другой стороны, позволяет с минимальными усилиями модифицировать программы под новые требования (как специализированные DSL).

                                      В этом смысле, ассемблеры — наиболее «жёсткие» языки.


                                      1. third112
                                        15.10.2017 17:07

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


                                        1. Hazactam
                                          15.10.2017 18:27

                                          Если исходить из того, что на ассемблере можно написать абсолютно всё, то да — он самый универсальный и есть. Другое дело — что он не кроссплатформенный, что тоже верно. Думаю — спор из-за термина 'универсальный'.


                                          1. third112
                                            15.10.2017 18:28

                                            Ok.


  1. Andrey_Epifantsev
    13.10.2017 17:48

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

    Как же они на это пошли? А как же священная обратная совместимость со старым кодом и с C?


    1. alexeykuzmin0
      13.10.2017 18:00

      Ну так-то удаленные фичи были как минимум со времен C++11.


      1. Idot
        13.10.2017 19:46

        А можно познакомиться со списком удалённого?


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


        1. khim
          14.10.2017 01:50

          А можно познакомиться со списком удалённого?
          Приложение С в стандарте этому посвящено.

          PS по Вашим словам, похоже практики настолько мало знакомы с нововведениями, что не успевают даже заметить и прочувствовать, что что-то добавили и удалили.
          Не совсем так. В C++11 удалили какие-то фичи, которые были обьявлены как «устаревшие» в C++03, в C++17 — то, что было обьявлено устаревшим в C++11.

          export template удалили, который ни одним компилятором не поддерживался (хотя вру — вроде один экспериментальный всё же был).


  1. Deamon87
    13.10.2017 22:35
    -1

    Это вы еще не видели brace инициализаторов в сочетании со структурами

     this->exteriorPortals.push_back({
                            groupId: i,
                            portalIndex : -1,
                            portalVertices: {},
                            frustumPlanes: frustumPlanes,
                            level : 0
                        });

    Вот это сейчас совершенно легальный код в С++. Привет JS?


    1. khim
      14.10.2017 01:51

      Это не валидный код в C++. Это всго лишь пропозал для C++20, причём в том виде, как вы его описали — это всё равно ошибка.

      И да — это весьма полезная фича, благо она есть в C99, активно используется во многих проектах, а в режиме C++ поддерживается, например, clang'ом.


      1. Deamon87
        14.10.2017 16:33

        Ну не знаю. Я могу предположить, что это самодеятельность разработчиков компилятора, но как минимум gcc 6 съедает такой код и не давится.


        Использование этой конструкции помогло мне очень эффективно портировать код с JS на C++.


        1. khim
          14.10.2017 23:11

          Я могу предположить, что это самодеятельность разработчиков компилятора, но как минимум gcc 6 съедает такой код и не давится.
          Прекрасно. В таком случае дать ссылку на https://godbolt.org/ вас, разумеется не затруднит. Дополните ваш пример до компилируемого кода — и можно будет что-то обсуждать.


          1. NeonMercury
            14.10.2017 23:35

            Вот, если я всё правильно понял: godbolt.org/g/ZSLJgN

            Если что, не я автор изначального комментария, но мне стало интересно и я проверил.


            1. khim
              15.10.2017 00:37

              Ого. Не ожидал. Clang тоже сьедает, кстати — но по крайней мере warning'и выдаёт.

              Это GNU'сное расширение, которое в стандарт C99 (а теперь и C++20) решили не вносить. Было принято решение использовать другой синтаксис, никакого отношения к JavaScript'у не имеющий.

              То есть в GCC реализовали инициализаторы с одной стороны криво и не полностью, а с другой — со своими собственными расширениями.

              Прэлестно, просто прэлестно.


              1. NeonMercury
                15.10.2017 15:26

                Да, я так же засомневался, что это стандартное поведение, но стандарт изучить не успел. Но так же подумал, что это один из вариантов инициализации через точку:

                struct A { int x; int y; int z; };
                A a{.y = 2, .x = 1};
                A b{.x = 1, .z = 2};
                


              1. f1inx
                16.10.2017 12:45

                Такие инициализаторы были доступны в GCC еще 20лет назад, а после выхода C99 они считаются deprecate и теперь нужно использовать C99 синтаксис.
                С чего вы взяли что они реализованы криво и не полностью?
                Если вы не знали что 90-99% новых стандартов C и C++ сначала обкатываются на GCC то это тоже, просто прэлестно. И «как ни странно» рано или поздно высокий процент расширений GCC попадают в стандарт.


  1. Cupper
    14.10.2017 02:55

    Constexpr if

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

    Не понял, зачем?
    template <typename T>
    T GetValue(T t) {
        return t;
    }
    
    template <typename T>
    T GetValue(T *t) {
        return *t;
    }
    
    int main()
    {
        int i = 5;
        
        std::cout << GetValue(i) << std::endl;
        std::cout << GetValue(&i) << std::endl;
    }


    1. EgorBredikhin Автор
      14.10.2017 16:47

      В общем случае такой подход не подойдет. Например для std::is_class или std::is_arithmetic.