- PVSM.RU - https://www.pvsm.ru -

Язык C++ постоянно развивается, и нам как разработчикам статического анализатора важно следить за всеми изменениями, чтобы поддерживать все новые возможности языка. В этой обзорной статье я хотел бы поделиться с читателем наиболее интересными нововведениями, появившимися в C++17, а также продемонстрировать их на примерах.
Сейчас поддержка нового стандарта активно добавляется разработчиками компиляторов. Посмотреть, что поддерживается на текущий момент, можно по ссылкам:
Для начала несколько слов о том, что вообще такое свертка списка (также известна как 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.
Теперь в шаблонах можно писать 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;
}
Стандартом было определено множество правил вывода типов [4] (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, и использовать вместо них конструктор.
В 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 [5] и 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 на порядок читабельнее.
До 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:
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();
}
};
В 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 или 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;
}
В 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 позволяет проверить, доступен ли заголовочный файл для подключения.
Приведу пример использования прямо из предложения к стандарту (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 предлагается использовать при работе с 'сырой' памятью. Обычно для этого используется char, unsigned char или uint8_t. Тип std::byte является более типобезопасным, так как к нему можно применить только побитовые операции, а арифметические операции и неявные преобразования недоступны. Другими словами, указатель на std::byte не удастся использовать в качестве фактического аргумента для вызова функции F(const unsigned char *).
Этот новый тип определен в <cstddef> [6]следующим образом:
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 появились новые правила, более строго определяющие порядок вычисления выражений:
Таким образом, как указывается в предложении к стандарту, в следующих выражениях теперь гарантированно сначала вычисляется 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. Ранее порядок вычисления таких подвыражений не был определен, что приводило к ошибкам.
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 << "yesn";
}
// Нерекурсивный обход директории
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 << "yesn";
}
// Копирование файла
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. Со всеми возможностями можно ознакомиться здесь [7].
Это шаблонный класс, который хранит опциональное значение. Его удобно использовать, чтобы, например, возвращать значение из функции, в которой может произойти какая-то ошибка:
// С++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 failedn";
}
return 0;
}
Еще у std::optional имеется метод value_or [8], который возвращает значение из optional, если оно доступно или иное установленное значение в противном случае.
Объект класса 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 — это шаблонный класс, который представляет собой 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;
}
В 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&.
В 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 было добавлено множество специализированных математических функций, таких как: бета-функции, Дзета-функции Римана и прочие. Подробнее о них прочитать можно здесь [9].
В C++17 можно написать:
namespace ns1::ns2
{
....
}
Вместо:
namespace ns1
{
namespace ns2
{
....
}
}
В 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-х значений:
Таким образом, чтобы получить многопоточную версию алгоритма, достаточно написать:
#include <iostream>
#include <vector>
#include <algorithm>
....
std::for_each(std::exception::par, vct.begin(), vct.end(),
[](auto &e) { e += 42; });
....
Необходимо следить, чтобы накладные расходы на создание потоков не перевесили выгоду от использования многопоточных алгоритмов. Естественно, также программисту самому нужно следить за тем, чтобы не возникало состояний гонки [10] или взаимных блокировок [11].
Также стоит отметить разницу между std::execution::seq и версией без такого параметра – если в функцию передается execution policy, то в этом алгоритме нельзя выбрасывать исключения, которые выходят за границы функтора. Если выбросить такое исключение, будет вызван std::terminate.
В связи с добавлением параллелизма, появилось несколько новых алгоритмов:
std::reduce – работает аналогично std::accumulate [12], но порядок свертки строго не определен, поэтому может работать параллельно. Имеет перегрузку, принимающую execution policy. Небольшой пример:
....
// Суммируем все элементы vct в параллельном режиме
std::reduce(std::exception::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 принимает на вход сущность, которая может быть вызвана, и набор аргументов и вызывает эту сущность с этими аргументами. Такими сущностями, например, являются указатель на функцию, объект с 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 << "hellon"; }); // 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;
}
В 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(arr, arr + 128, 3.14f);
if (!res1.ec)
{
std::cout << arr << 'n';
}
float val;
auto res2 = std::from_chars(arr, arr + 128, val);
if (!res2.ec)
{
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 принимает на вход ссылку и возвращает ссылку на константу:
// C++17
#include <utility>
....
MyObject obj{ 42 };
const MyObject& constView = std::as_const(obj);
....
В дополнение к уже существующим свободным функциям 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;
}
В 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::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 для атрибутов, поэтому при использовании нескольких атрибутов можно немного сократить запись. Пример из предложения к стандарту (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 возвращает ссылку на вставленный элемент, до 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";
// Стандартный поиск
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 вызывает с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;
}
В 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;
}
В C++17 появилась функция std::not_fn, возвращающая предикат-отрицание. Эта функция призвана заменить std::not1 [13] и std::not2 [14]:
#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;
}
В С++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, "aaa"},
{2, "bb"},
{3, "ccc"},
{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(a == 42, "a must be equal to 42");
static_assert(a == 42); // Теперь можно писать так
static_assert ( constant-expression ) ;
static_assert ( constant-expression , string-literal ) ;
В 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");
Теперь 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;
}
В 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;
}
К сожалению, в C++17 не вошли ожидаемые всеми модули, концепты, работа с сетью, рефлексия и прочие важные фичи, поэтому с нетерпением ждем C++20.
Для себя, как одного из разработчиков анализатора кода PVS-Studio [16], могу отметить, что нам предстоит много интересной работы. Новые возможности языка открывают и новые возможности «отстрелить себе ногу», и мы должны научить анализатор предупреждать программиста об ошибках новых разновидностей. Например, в C++14 появилась возможность инициализировать динамический массив при его создании. Следовательно, полезно предупреждать программиста, когда размер динамического массива может оказаться меньше количества элементов в его инициализаторе. Поэтому мы создали новую диагностику V798 [17]. Диагностики для новых конструкций языка мы делали и продолжаем делать. Для C++17 будет полезно, например, предупредить, что в алгоритме для std::execution::par используются конструкции, которые могут сгенерировать исключения и эти исключения не будут специально перехвачены внутри алгоритма с помощью try...catch.
Спасибо за внимание. Предлагаю скачать [18] PVS-Studio (Windows/Linux) и проверить свои проекты. Язык C++ становится все «более большим» и все сложнее отследить все аспекты и нюансы его использования, чтобы написать правильный код. PVS-Studio содержит большую базу знаний о том, «что делать нельзя» и будет вам незаменимым помощником. Да и от простых опечаток никто не застрахован и никуда эта проблема не денется. Proof [19].
Автор: EgorBredikhin
Источник [26]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/pvs-studio/265578
Ссылки в тексте:
[1] GCC: https://gcc.gnu.org/projects/cxx-status.html
[2] Clang: https://clang.llvm.org/cxx_status.html
[3] Visual Studio: https://docs.microsoft.com/en-us/cpp/visual-cpp-language-conformance
[4] правил вывода типов: http://en.cppreference.com/w/cpp/language/class_template_argument_deduction
[5] SFINAE: https://en.wikipedia.org/wiki/Substitution_failure_is_not_an_error
[6] <cstddef>: http://en.cppreference.com/w/cpp/header/cstddef
[7] здесь: http://en.cppreference.com/w/cpp/header/filesystem
[8] value_or: http://en.cppreference.com/w/cpp/utility/optional/value_or
[9] здесь: http://en.cppreference.com/w/cpp/numeric/special_math
[10] состояний гонки: https://www.viva64.com/ru/t/0042/
[11] взаимных блокировок: https://www.viva64.com/ru/t/0013/
[12] std::accumulate: http://en.cppreference.com/w/cpp/algorithm/accumulate
[13] std::not1: http://en.cppreference.com/w/cpp/utility/functional/not1
[14] std::not2: http://en.cppreference.com/w/cpp/utility/functional/not2
[15] триграфы: http://en.cppreference.com/w/cpp/language/operator_alternative
[16] PVS-Studio: https://www.viva64.com/ru/pvs-studio/
[17] V798: https://www.viva64.com/ru/w/V798/
[18] скачать: https://www.viva64.com/ru/pvs-studio-download/
[19] Proof: https://www.viva64.com/ru/b/0509/
[20] Changes between C++14 and C++17 DIS: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0636r1.html
[21] Полухин Антон | C++17: https://youtu.be/GK9gtIrJaBk
[22] Part 1: https://youtu.be/WDGP5pO1TyM
[23] Part 2: https://youtu.be/_PEgl63V7wc
[24] Trip report: Summer ISO C++ standards meeting (Oulu): https://herbsutter.com/2016/06/30/trip-report-summer-iso-c-standards-meeting-oulu/
[25] C++ 17 Features: http://www.bfilipek.com/2017/01/cpp17features.html
[26] Источник: https://habrahabr.ru/post/340014/
Нажмите здесь для печати.