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

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

Свернуть выражения

Я хотел бы начать с нескольких слов о том, что такое сворачивание (также известное как сокращение или накопление).

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

Пример из 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 init)

Бинарная правоассоциативная складка

(init op… op pack)

Бинарная левоассоциативная складка

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

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

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)), ...);
//This code is expanded into a sequence of expressions      
//separated by commas as follows:
  //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;
}

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

шаблон ‹auto›

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

// C++17
template<auto n>
void Func() { /* .... */ }
int main()
{
  Func<42>(); // will deduce int type
  Func<'c'>(); // will deduce char type
  return 0;
}

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

// 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;
}

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

// C++17
#include <iostream>
template<typename T, typename U>
struct S
{
  T m_first;
  U m_second;
};
// My 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;
}

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

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

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

Constexpr, если

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:

// C++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:

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

встроенные переменные

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

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

(Вместо того, чтобы писать 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.

Структурированные привязки

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

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

// 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, если элемент не был вставлен (ig уже содержится в 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;
}

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

// 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 (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). В этом примере мы включаем «необязательный», если он доступен:

#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]], [[supports_dependency]] и [[deprecated]], в C ++ 17 появилось дерево новых атрибутов:

[[падение]]

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

Быстрый пример:

// C++17
switch (i)
{
case 10:
  f1();
  break;
case 20:
  f2();
  break;
case 30:
  f3();
  break;
case 40:
  f4();
  [[fallthrough]]; // The warning will be suppressed
case 50:
  f5();
}

[[nodiscard]]

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

// C++17
[[nodiscard]] int Sum(int a, int b)
{
  return a + b;
}
int main()
{
  Sum(5, 6); // Compiler/analyzer warning will be issued
  return 0;
}

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

// C++17
struct [[nodiscard]] NoDiscardType
{
  char a;
  int b;
};
NoDiscardType Func()
{
  return {'a', 42};
}
int main()
{
  Func(); // Compiler/analyzer warning will be issued
  
  return 0;
}

[[возможно_unused]]

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

// The warning will be suppressed 
[[maybe_unused]] static void SomeUnusedFunc() { .... }
// The warning will be suppressed
void Foo([[maybe_unused]] int a) { .... }
void Func()
{
  // The warning will be suppressed
  [[maybe_unused]] int someUnusedVar = 42;
  ....
}

std: байтовый тип

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

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

enum class byte : unsigned char {};

Динамическое выделение памяти для перевыровненных типов

В 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 вводит новые правила, более строго определяющие порядок вычисления выражений:

  • Выражения Postfix оцениваются слева направо (включая вызовы функций и доступ к членам объектов)
  • Выражения присваивания оцениваются справа налево.
  • Операнды операторов ‹< и ›› оцениваются слева направо.

Таким образом, как указано в предложении по стандарту, в следующих выражениях теперь гарантированно оценивается сначала 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");

Это код из книги Страуструпа «Язык программирования C ++, 4-е издание», который использовался для демонстрации вызова методов в цепочке. Раньше этот код вел неопределенное поведение; начиная с C ++ 17 он будет работать по назначению. Проблема заключалась в том, что было неясно, какая из функций find будет вызываться первой.

Итак, теперь в таких выражениях:

obj.F1(subexpr1).F2(subexpr2).F3(subexpr3).F4(subexpr4)

Подвыражения подвыражение1, подвыражение2, подвыражение3, подвыражение4 оцениваются в соответствии с порядком вызова F1 , F2, F3, F4 функции. Ранее порядок вычисления таких выражений не был определен, что приводило к ошибкам.

Файловая система

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'; // It'll print "/dir1/dir2"
cout << file_path.filename() << '\n'; // It'll print "file.txt"
cout << file_path.extension() << '\n'; // It'll print ".txt"
file_path.replace_filename("file2.txt");
file_path.replace_extension(".cpp");
cout << file_path << '\n'; // It'll print "/dir1/dir2/file2.cpp"
fs::path dir_path("/dir1");
dir_path.append("dir2/file.txt");
cout << dir_path << '\n'; // It'll print "/dir1/dir2/file.txt"

Работа с каталогами:

// Getting the current working directory
fs::path current_path = fs::current_path();
// Creating a directory
fs::create_directory("/dir");
// Creating several directories
fs::create_directories("/dir/subdir1/subdir2");
// Verifying the existence of a directory
if (fs::exists("/dir/subdir1"))
{
  cout << "yes\n";
}
// Non-recursive directory traversal
for (auto &p : fs::directory_iterator(current_path))
{
  cout << p.path() << '\n';
}
// Recursive directory traversal
for (auto &p : fs::recursive_directory_iterator(current_path))
{
  cout << p.path() << '\n';
}
// Nonrecursive directory copy
fs::copy("/dir", "/dir_copy");
// Recursive directory copy
fs::copy("/dir", "/dir_copy", fs::copy_options::recursive);
// Removal of the directory with all contents, if it exists
fs::remove_all("/dir");

Возможные значения fs :: copy_options для обработки уже существующих файлов представлены в таблице:

Постоянный

Ценить

никто

Если файл уже существует, выдается исключение. (Значение по умолчанию)

skip_existing

Существующие файлы не перезаписываются, и исключение не создается.

overwrite_existing

Существующие файлы перезаписываются.

update_existing

Существующие файлы перезаписываются только новыми файлами.

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

// Verifying the existence of a file
if (fs::exists("/dir/file.txt"))
{
  cout << "yes\n";
}
// Copying a file
fs::copy_file("/dir/file.txt", "/dir/file_copy.txt",
  fs::copy_options::overwrite_existing);
// Getting the file size (in bytes)
uintmax_t size = fs::file_size("/dir/file.txt");
// Renaming a file
fs::rename("/dir/file.txt", "/dir/file2.txt");
// Deleting a file if it exists
fs::remove("/dir/file2.txt");

Это далеко не полный список возможностей std :: filesystem. Все функции можно найти здесь.

std :: optional

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

// C++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" will be displayed
  return 0;
}

std :: option

std :: variant - это класс шаблона, представляющий собой объединение, которое запоминает, какой тип он хранит. Кроме того, в отличие от union, std :: variant позволяет хранить типы, не относящиеся к POD.

#include <iostream>
#include <variant>
int main()
{
  // stores either int, or float or 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 *, так что больше нет необходимости писать три перегруженные функции:

// 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, потому что это повысит производительность в тех случаях, когда строка литерал передается в функцию или C-массив. Это связано с тем, что выделение памяти обычно происходит при построении объекта 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 не использует аргумент только для перемещения в случае, если вставка элемента не произошла. Лучше всего это объяснить на примере:

// 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
  {
    ....
  }
}

Неконстантная строка :: данные

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

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

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

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

Функции из ‹algorithm›, работающие с контейнерами, теперь имеют многопоточные версии. Всем им была предоставлена ​​дополнительная перегрузка, которая принимает политику выполнения в качестве первого аргумента, который определяет способ работы алгоритма.

Политика выполнения может иметь одно из трех значений:

  • std :: execution :: seq - последовательное выполнение
  • std :: execution :: par - параллельное выполнение
  • 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, и версией без такого параметра; если в функцию передается политика выполнения, в исключениях этого алгоритма, выходящих за границы объекта функции, она не должна создаваться. Если возникает такое исключение, вызывается std :: terminate.

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

std :: reduce работает так же, как std :: accumulate, но порядок не определен строго, поэтому он может работать параллельно. У него также есть перегрузка, которая принимает политику выполнения. Небольшой пример:

....
// Summing up all the vct elements in the parallel mode
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 - это код ошибки

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

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

Помимо уже существующих бесплатных функций s td :: 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 };
  // should be used for C-style arrays.
  size_t sz2 = std::size(a1);
  return 0;
}

std :: зажим

В 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;
}

Метафункции логической операции

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

// 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 :: конъюнкция и std :: disjunction остановят создание экземпляра, как только можно будет определить результирующее значение. .

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

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

// C++17
#include <iostream>
enum E
{
  A = 0,
  B = 1,
  C = 2,
  First[[deprecated]] = A,
};
namespace[[deprecated]] DeprecatedFeatures
{
  void OldFunc() {};
//....
}
int main()
{
  // Compiler warning will be issued
  DeprecatedFeatures::OldFunc();
  
  // Compiler warning will be issued
  std::cout << E::First << '\n'; 
  return 0;
}

Использование префикса для атрибутов

Для атрибутов добавлено использование префикса, поэтому, если вы используете несколько атрибутов, вы можете немного уменьшить количество кода. Пример из предложения по стандарту (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 << ' ';
  }
}

Функциональные объекты для поиска подстроки в строке (поисковые объекты-функции)

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

#include <iostream>
#include <string>
#include <algorithm>
#include <functional>
int main()
{
  std::string haystack = "Hello, world!";
  std::string needle = "world";
  // Standard search
  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()));
  // Search using the Boyer-Moore algorithm
  auto it3 = std::search(haystack.begin(), haystack.end(),
    std::boyer_moore_searcher(needle.begin(), needle.end()));
  // Search using the Boyer-Moore algorithm-Horspula
  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 вызывает вызываемый объект с набором параметров, хранящихся в кортеже. Пример:

#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 (Универсальный отрицатель 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;
}

Доступ к узлам контейнеров (дескриптор узла)

В C ++ 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, и вставить их. в другой контейнер с помощью вставки:

// 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"}, {3, "Andrew"}, {42, "Peter"}};
  return 0;
}

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

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

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

static_assert(a == 42, "a must be equal to 42");
static_assert(a == 42); // Now you write like this
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 для массивов

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, который блокирует несколько мьютексов одновременно (с помощью блокировки) во время создания и освобождает их все в деструкторе, обеспечивая удобный 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 automatically frees when exiting block
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 включает в себя обширную базу знаний о том, чего нельзя делать, и он станет вам незаменимым помощником. К тому же от простых опечаток никто не застрахован, и эта проблема никуда не делась. "Доказательство".

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

Егор Бредихин