Pull to refresh
312.86
PVS-Studio
Static Code Analysis for C, C++, C# and Java

C++17

Reading time 26 min
Views 85K

Рисунок 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"}, {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); // Теперь можно писать так
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. Пожалуйста, ознакомьтесь со списком.
Tags:
Hubs:
+82
Comments 177
Comments Comments 177

Articles

Information

Website
pvs-studio.com
Registered
Founded
2008
Employees
31–50 employees