Язык 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-х значений:
- 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 и версией без такого параметра – если в функцию передается execution policy, то в этом алгоритме нельзя выбрасывать исключения, которые выходят за границы функтора. Если выбросить такое исключение, будет вызван std::terminate.
В связи с добавлением параллелизма, появилось несколько новых алгоритмов:
std::reduce – работает аналогично std::accumulate, но порядок свертки строго не определен, поэтому может работать параллельно. Имеет перегрузку, принимающую execution policy. Небольшой пример:
....
// Суммируем все элементы vct в параллельном режиме
std::reduce(std::execution::par, vct.begin(), vct.end())
....
std::transform_reduce – применяет заданный функтор на элементах контейнера, а затем применяет std::reduce.
std::for_each_n – работает аналогично std::for_each, но заданный функтор применяется только к n элементам. Например:
....
std::vector<int> vct = { 1, 2, 3, 4, 5 };
std::for_each_n(vct.begin(), 3, [](auto &e) { e *= 10; });
// vct: {10, 20, 30, 4, 5}
....
std::invoke, трейт is_invocable
std::invoke принимает на вход сущность, которая может быть вызвана, и набор аргументов и вызывает эту сущность с этими аргументами. Такими сущностями, например, являются указатель на функцию, объект с operator(), лямбда-функция и прочие:
// C++17
#include <iostream>
#include <functional>
int Func(int a, int b)
{
return a + b;
}
struct S
{
void operator() (int a)
{
std::cout << a << '\n';
}
};
int main()
{
std::cout << std::invoke(Func, 10, 20) << '\n'; // 30
std::invoke(S(), 42); // 42
std::invoke([]() { std::cout << "hello\n"; }); // hello
return 0;
}
std::invoke может пригодиться в какой-нибудь шаблонной магии. Также в C++17 был добавлен трейт std::is_invocable:
// C++17
#include <iostream>
#include <type_traits>
void Func() { };
int main()
{
std::cout << std::is_invocable<decltype(Func)>::value << '\n'; // 1
std::cout << std::is_invocable<int>::value << '\n'; // 0
return 0;
}
std::to_chars, std::from_chars
В C++17 появились функции std::to_chars и std::from_chars для очень быстрого преобразования чисел в строки и строк в числа соответственно. В отличие от других функций форматирования из C и C++, std::to_chars не зависит от локали, не выделяет память и не выбрасывает исключений, и нацелены на максимальную производительность:
// C++17
#include <iostream>
#include <charconv>
int main()
{
char arr[128];
auto res1 = std::to_chars(std::begin(arr), std::end(arr), 3.14f);
if (res1.ec != std::errc::value_too_large)
{
std::cout << arr << '\n';
}
float val;
auto res2 = std::from_chars(std::begin(arr), std::end(arr), val);
if (res2.ec != std::errc::invalid_argument &&
res2.ec != std::errc::result_out_of_range)
{
std::cout << arr << '\n';
}
return 0;
}
Функция std::to_chars возвращает структуру to_chars_result:
struct to_chars_result
{
char* ptr;
std::errc ec;
};
ptr – указатель на последний записанный символ + 1
ec – код ошибки
Функция std::from_chars возвращает структуру from_chars_result:
struct from_chars_result
{
const char* ptr;
std::errc ec;
};
ptr – указатель на первый символ, не удовлетворяющий паттерну
ec – код ошибки
На мой взгляд, стоит использовать эти функции везде, где нужны преобразования из строки в число и из числа в строку, в случаях, когда вам достаточно Си-локали, т.к. это даст неплохой прирост производительности.
std::as_const
Вспомогательная функция std::as_const принимает на вход ссылку и возвращает ссылку на константу:
// C++17
#include <utility>
....
MyObject obj{ 42 };
const MyObject& constView = std::as_const(obj);
....
Свободные функции std::size, std::data и std::empty
В дополнение к уже существующим свободным функциям std::begin, std::end и прочим появились свободные функции std::size, std::data и std::empty:
// C++17
#include <vector>
int main()
{
std::vector<int> vct = { 3, 2, 5, 1, 7, 6 };
size_t sz = std::size(vct);
bool empty = std::empty(vct);
auto ptr = std::data(vct);
int a1[] = { 1, 2, 3, 4, 5, 6 };
// стоит использовать для C-style массивов.
size_t sz2 = std::size(a1);
return 0;
}
std::clamp
В C++17 появилась функция std::clamp(x, low, high), которая возвращает x, если он находится в интервале [low, high] или ближайшее из этих значений в противном случае:
// C++17
#include <iostream>
#include <algorithm>
int main()
{
std::cout << std::clamp(7, 0, 10) << '\n'; // 7
std::cout << std::clamp(7, 0, 5) << '\n'; //5
std::cout << std::clamp(7, 10, 50) << '\n'; //10
return 0;
}
НОД и НОК
В стандарте появилось вычисление Наибольшего Общего Делителя (std::gcd) и Наименьшего Общего Кратного (std::lcm):
// C++17
#include <iostream>
#include <numeric>
int main()
{
std::cout << std::gcd(24, 60) << '\n'; // 12
std::cout << std::lcm(8, 10) << '\n'; // 40
return 0;
}
Логические метафункции (Logical operation metafunctions)
В C++17 появились логические метафункции std::conjunction, std::disjunction и std::negation. Они используются для того, чтобы выполнить логическое И, ИЛИ, НЕ на наборе трейтов соответственно. Небольшой пример с std::conjunction:
// C++17
#include <iostream>
#include <string>
#include <algorithm>
#include <functional>
template<typename... Args>
std::enable_if_t<std::conjunction_v<std::is_integral<Args>...>>
Func(Args... args)
{
std::cout << "All types are integral.\n";
}
template<typename... Args>
std::enable_if_t<!std::conjunction_v<std::is_integral<Args>...>>
Func(Args... args)
{
std::cout << "Not all types are integral.\n";
}
int main()
{
Func(42, true); // All types are integral.
Func(42, "hello"); // Not all types are integral.
return 0;
}
Замечу, что в отличие от свертки параметров шаблона, упомянутой выше, функции std::conjunction и std::disjunction остановят инстанцирование, как только результирующее значение сможет быть определено.
Атрибуты в пространствах имен и перечислениях
Теперь можно использовать атрибуты для пространств имен и для перечислений, а также внутри них:
// C++17
#include <iostream>
enum E
{
A = 0,
B = 1,
C = 2,
First[[deprecated]] = A,
};
namespace[[deprecated]] DeprecatedFeatures
{
void OldFunc() {};
//....
}
int main()
{
// Будет выдано предупреждение компилятора
DeprecatedFeatures::OldFunc();
// Будет выдано предупреждение компилятора
std::cout << E::First << '\n';
return 0;
}
Префикс using для атрибутов
Добавлен префикс using для атрибутов, поэтому при использовании нескольких атрибутов можно немного сократить запись. Пример из предложения к стандарту (P0028R4):
// C++14
void f()
{
[[rpr::kernel, rpr::target(cpu, gpu)]]
task();
}
// C++17
void f()
{
[[using rpr:kernel, target(cpu, gpu)]]
task();
}
Возвращаемое значение у emplace_back
Теперь emplace_back возвращает ссылку на вставленный элемент, до C++17 он не возвращал никакого значения:
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vct = { 1, 2, 3 };
auto &r = vct.emplace_back(10);
r = 42;
for (const auto &i : vct)
{
std::cout << i << ' ';
}
}
Функторы для поиска подстроки в строке (Searcher functors)
В C++17 появились функторы, реализующие поиск подстроки в строке, использующие алгоритм Бойера – Мура или алгоритм Бойера — Мура – Хорспула. Эти функторы можно передавать в std::search:
#include <iostream>
#include <string>
#include <algorithm>
#include <functional>
int main()
{
std::string haystack = "Hello, world!";
std::string needle = "world";
// Стандартный поиск
auto it1 = std::search(haystack.begin(), haystack.end(),
needle.begin(), needle.end());
auto it2 = std::search(haystack.begin(), haystack.end(),
std::default_searcher(needle.begin(), needle.end()));
// Поиск с использованием алгоритма Бойера - Мура
auto it3 = std::search(haystack.begin(), haystack.end(),
std::boyer_moore_searcher(needle.begin(), needle.end()));
// Поиск с использованием алгоритма Бойера - Мура - Хорспула
auto it4 = std::search(haystack.begin(), haystack.end(),
std::boyer_moore_horspool_searcher(needle.begin(), needle.end()));
std::cout << it1 - haystack.begin() << '\n'; // 7
std::cout << it2 - haystack.begin() << '\n'; // 7
std::cout << it3 - haystack.begin() << '\n'; // 7
std::cout << it4 - haystack.begin() << '\n'; // 7
return 0;
}
std::apply
std::apply вызывает сallable-объект с набором параметров, записанным в кортеже. Пример:
#include <iostream>
#include <tuple>
void Func(char x, int y, double z)
{
std::cout << x << y << z << '\n';
}
int main()
{
std::tuple args{ 'c', 42, 3.14 };
std::apply(Func, args);
return 0;
}
Конструирование объектов из кортежей (std::make_from_tuple)
В C++17 появилась возможность сконструировать объект, передав в конструктор набор аргументов, записанных в кортеже. Для этого используется функция std::make_from_tuple:
#include <iostream>
#include <tuple>
struct S
{
char m_x;
int m_y;
double m_z;
S(char x, int y, double z) : m_x(x), m_y(y), m_z(z) {}
};
int main()
{
std::tuple args{ 'c', 42, 3.14 };
S s = std::make_from_tuple<S>(args);
std::cout << s.m_x << s.m_y << s.m_z << '\n';
return 0;
}
std::not_fn (Universal negator not_fn)
В C++17 появилась функция std::not_fn, возвращающая предикат-отрицание. Эта функция призвана заменить std::not1 и std::not2:
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
bool LessThan10(int a)
{
return a < 10;
}
int main()
{
std::vector vct = { 1, 6, 3, 8, 14, 42, 2 };
auto n = std::count_if(vct.begin(), vct.end(),
std::not_fn(LessThan10));
std::cout << n << '\n'; // 2
return 0;
}
Доступ к нодам контейнеров (Node handle)
В С++17 появилась возможность перемещать ноду напрямую из одного контейнера в другой. При этом не происходят дополнительные аллокации или копирование. Приведу небольшой пример:
// C++17
#include <map>
#include <string>
int main()
{
std::map<int, std::string> myMap1{ { 1, "aa" },
{ 2, "bb" },
{ 3, "cc" } };
std::map<int, std::string> myMap2{ { 4, "dd" },
{ 5, "ee" },
{ 6, "ff" } };
auto node = myMap1.extract(2);
myMap2.insert(std::move(node));
// myMap1: {{1, "aa"}, {3, "cc"}}
// myMap2: {{2, "bb"}, {4, "dd"}, {5, "ee"}, {6, "ff"}}
return 0;
}
Метод std::extract позволяет извлечь ноду из контейнера, а метод insert теперь также умеет вставлять ноды.
Также в C++17 у контейнеров появился метод merge, который пытается извлечь все ноды контейнера с помощью extract и вставить их в другой контейнер с помощью insert:
// C++17
#include <map>
#include <string>
int main()
{
std::map<int, std::string> myMap1{ { 1, "aa" },
{ 2, "bb" },
{ 3, "cc" } };
std::map<int, std::string> myMap2{ { 4, "dd" },
{ 5, "ee" },
{ 6, "ff" } };
myMap1.merge(myMap2);
// myMap1: {{1, "aa"},
// {2, "bb"},
// {3, "cc"},
// {4, "dd"},
// {5, "ee"},
// {6, "ff"}}
// myMap2: {}
return 0;
}
Еще одним интересным примером может служить изменение ключа элемента в std::map:
// C++17
#include <map>
#include <string>
int main()
{
std::map<int, std::string> myMap{ { 1, "Tommy" },
{ 2, "Peter" },
{ 3, "Andrew" } };
auto node = myMap.extract(2);
node.key() = 42;
myMap.insert(std::move(node));
// myMap: {{1, "Tommy"}, {42, "Peter"}, {3, "Andrew"}};
return 0;
}
До C++17 избежать дополнительных накладных расходов при изменении ключа было невозможно.
static_assert с одним аргументом
Теперь для static_assert необязательно указывать сообщение:
static_assert(a == 42, "a must be equal to 42");
static_assert(a == 42); // Теперь можно писать так
static_assert ( constant-expression ) ;
static_assert ( constant-expression , string-literal ) ;
std::*_v<T...>
В C++17 у всех трейтов из <type_traits>, имеющих поле ::value, появились перегрузки вида some_trait_v<T>. Поэтому теперь вместо того, чтобы писать some_trait<T>::value, можно просто написать some_trait_v<T>. Например:
// C++14
static_assert(std::is_integral<T>::value, "Integral required.");
// C++17
static_assert(std::is_integral_v<T>, "Integral required");
std::shared_ptr for arrays
Теперь shared_ptr поддерживает C-массивы. Необходимо просто передать T[] шаблонным параметром и shared_ptr вызовет delete[] при освобождении памяти. Ранее для массивов нужно было указывать функцию для удаления вручную. Небольшой пример:
#include <iostream>
#include <memory>
int main()
{
// C++14
//std::shared_ptr<int[]> arr(new int[7],
// std::default_delete<int[]>());
// C++17
std::shared_ptr<int[]> arr(new int[7]);
arr.get()[0] = 1;
arr.get()[1] = 2;
arr.get()[2] = 3;
....
return 0;
}
std::scoped_lock
В C++17 появился новый класс scoped_lock, который блокирует несколько мьютексов одновременно (используя lock) при создании и освобождает их всех в деструкторе, предоставляя удобный RAII-интерфейс. Небольшой пример:
#include <thread>
#include <mutex>
#include <iostream>
int var;
std::mutex varMtx;
void ThreadFunc()
{
std::scoped_lock lck { varMtx };
var++;
std::cout << std::this_thread::get_id() << ": " << var << '\n';
} // <= varMtx автоматически освобождается при выходе из блока
int main()
{
std::thread t1(ThreadFunc);
std::thread t2(ThreadFunc);
t1.join();
t2.join();
return 0;
}
Удаленные возможности
- Были удалены триграфы.
- Ключевое слово register больше нельзя использовать как спецификатор переменной. Оно остается зарезервированным на будущее, как это было с auto.
- Были удалены префиксный и постфиксный инкременты для типа bool.
- Была удалена спецификация исключений. Больше нельзя указать какие именно исключения выбрасывает функция. В C++17 стоит лишь помечать функции, которые не выбрасывают исключений как noexcept.
- Был удален std::auto_ptr. Вместо него стоит использовать std::unique_ptr.
- Был удален std::random_shuffle. Вместо него стоит использовать std::shuffle, с соответствующим функтором, генерирующим случайные числа. Удаление связанно с тем, что std::random_shuffle использовал std::rand, который в свою очередь признан устаревшим.
Итоги
К сожалению, в C++17 не вошли ожидаемые всеми модули, концепты, работа с сетью, рефлексия и прочие важные фичи, поэтому с нетерпением ждем C++20.
Для себя, как одного из разработчиков анализатора кода PVS-Studio, могу отметить, что нам предстоит много интересной работы. Новые возможности языка открывают и новые возможности «отстрелить себе ногу», и мы должны научить анализатор предупреждать программиста об ошибках новых разновидностей. Например, в C++14 появилась возможность инициализировать динамический массив при его создании. Следовательно, полезно предупреждать программиста, когда размер динамического массива может оказаться меньше количества элементов в его инициализаторе. Поэтому мы создали новую диагностику V798. Диагностики для новых конструкций языка мы делали и продолжаем делать. Для C++17 будет полезно, например, предупредить, что в алгоритме для std::execution::par используются конструкции, которые могут сгенерировать исключения и эти исключения не будут специально перехвачены внутри алгоритма с помощью try...catch.
Спасибо за внимание. Предлагаю скачать PVS-Studio (Windows/Linux) и проверить свои проекты. Язык C++ становится все «более большим» и все сложнее отследить все аспекты и нюансы его использования, чтобы написать правильный код. PVS-Studio содержит большую базу знаний о том, «что делать нельзя» и будет вам незаменимым помощником. Да и от простых опечаток никто не застрахован и никуда эта проблема не денется. Proof.
Дополнительные ссылки
- Changes between C++14 and C++17 DIS.
- Youtube. Полухин Антон | C++17.
- Youtube. Nicolai Josuttis. С++17. The Language Features. Part 1, Part 2.
- Herb Sutter. Trip report: Summer ISO C++ standards meeting (Oulu).
- Bartlomiej Filipek. C++ 17 Features.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Egor Bredikhin. C++17
Комментарии (171)
ababo
13.10.2017 11:50Чувствуется влияние Golang и Rust.
DaylightIsBurning
13.10.2017 16:32+5<
sarcasm>особенно golang</sarcasm>
ababo
13.10.2017 18:15-2Только не включайте больше ваш сарказм, пожалуйста. А просто перечитайте про инициализаторы в if и switch.
netch80
13.10.2017 19:20-1Возможность «инициализатор в if и switch» — это как раз если не впервые в таком виде появилось в Go, то по крайней мере там стало хорошо известно — и доказало свою полезность.
Или Вы знаете другой язык из популярных, где это было раньше?bfDeveloper
13.10.2017 19:28+3Инициализаторы в if в C++ были очень давно. Не хватало возможности определить условие, отличное от приведения проинициализированной переменной к bool.
netch80
13.10.2017 19:33-1Ну то есть они были практически бесполезны.
А вот именно в такой удобной форме (и с блоком на весь if/switch) — образцом послужил Go.qw1
13.10.2017 20:38С указателями получалось очень даже красиво:
if (foo* p = findFoo(id)) { p->bar(); }
khim
14.10.2017 01:28Вообще-то образцов послужил C++98, в котором это было разрешено делать в цикле
for
.
Когда эта фича в C++ появилась — ни rust'а, ни go даже в проекте не было.
DaylightIsBurning
13.10.2017 21:14if (y = f(x), y > x) { ... // statements involving x and y }
netch80
13.10.2017 21:28Упускаете главный момент.
Новая форма аналогична созданию блока, в котором определены новые переменные, а после этого вложен собственно if. Пример автора статьи:
if (auto it = m.find(key); it != m.end()) { .... }
аналогичен
{ auto if = m.find(key); if (it != m.end()) { .... } // кстати, тут может быть else-ветка, или даже цепочка else if ... else, // в которой эта переменная будет видна }
но блок не выписывается явно.
По выходу из блока сработают деструкторы; кроме того, новая переменная не будет видна после завершения if — чтобы случайно её не применить где не следует.
Потому — это чисто «сахар». Но очень практически полезный, раз ввели.
Кстати, насколько я понял final draft, несколько отдельных init-statement ввести нельзя. Немного жаль.DaylightIsBurning
13.10.2017 21:38Я с этим и не спорю. С моей точки зрения это никак не связано с go, все совпадения с go случайны :).
netch80
13.10.2017 21:44А Вы где-то ещё видели именно такой же синтаксис — две части через точку с запятой, первая необязательна, но должна что-то инициализировать? Я — только в одном источнике, и его тут уже назвали.
Был бы другой образец — взяли бы его, потому что ничто не мешало, например, создать отдельное ключевое слово и блок за ним (что было бы как-то более понятно при отсутствии такого образца), или сделать другое построение блока (хм, а почему if и switch, но не while?), перетащить GCC вариант ({...}) с уточнением блочного контекста, и т.п.DaylightIsBurning
13.10.2017 21:55+1ну, например, тут:
for(int i=f(x); i<10;) {;;}
распространение этого синтаксиса и на if является вполне логичным продолжением даже без влияния go. Тот факт, что что-то в новом C++ похоже на go ещё не означает, что они двигаются в одном направлении и даже не значит, что именно оттуда оно заимствуется:
Цитирую пропозал:
There are three statements in C++, if, for and while, which are all variations on a theme. We propose to make the picture more complete by adding a new form of if statement.
netch80
13.10.2017 22:12> распространение этого синтаксиса и на if является вполне логичным продолжением даже без влияния go.
Для меня тут колоссальной разницей является то, что в for три части присутствуют обязательно (даже если пустые), а в if первая может отсутствовать. Именно конкретный метод решения был взят в конкретном месте. Альтернатива, в виде названного в том же пропозале with(), присутствовала в других местах.
И я не могу назвать его «логичным продолжением», больше похоже на достаточно злобный хак — хотя бы потому, что усложняет грамматику.
> There are three statements in C++, if, for and while, which are all variations on a theme.
И он тут же исключает из рассмотрения else для if… мне эта логика ой не кажется корректной.
К слову, else для for и while (по образцу Питона) иногда тоже очень полезно. :)
DaylightIsBurning
13.10.2017 22:20А Вы где-то ещё видели именно такой же синтаксис — две части через точку с запятой,
А какое это имеет отношение к делу? После не значит в следствии. Мысль о том, что if не хватает инициализации посещала меня ещё до появления golang. Да и сами авторы пропозала тоже мотивируют внутренними потребностями языка.
eao197
13.10.2017 11:57Вот от этого фрагмента прям какой-то теплой ламповой сишечкой повеяло:
Может имело бы смысл его записать в духе современного C++?auto res1 = std::to_chars(arr, arr + 128, 3.14f);
Как-то понадежнее будет.auto res1 = std::to_chars(std::begin(arr), std::end(arr), 3.14f);
EgorBredikhin Автор
13.10.2017 12:26Да, согласен, спасибо. Подправил в статье.
a1ien_n3t
13.10.2017 12:49Очень странно что to_chars возвражает структуру, а не std::pair
Тогда бы и тут можно было писать что-то типа
auto [ptr, error] = std::to_chars(...)EgorBredikhin Автор
13.10.2017 12:54+3Так написать можно. Structured bindings, как было сказано в статье, умеют производить декомпозицию типов, содержащих только нестатические открытые члены.
TargetSan
13.10.2017 12:48Меня больше бомбануло от
struct to_chars_result { char* ptr; std::errc ec; };
На вход итераторы, а на выход — поинтер… Слов нет, одни выражения. А, ну и голый эррор код. С++ так и не смог нормальную единую обработку ошибок, сплошные костыли.
EgorBredikhin Автор
13.10.2017 13:01+1На вход to_chars подаются тоже указатели. А вообще, to_chars была так спроектированна именно для максимально быстрого преобразования.
TargetSan
13.10.2017 13:05Тогда могли бы через
string_view
. И возвращать тоже частьstring_view
. Увы, много рассказывают про "безопасные практики" — при этом периодически добавляя способов отстрелить себе ногу.
eao197
13.10.2017 13:04Ну, вообще-то, to_chars получает на вход char*. Что получил, то и вернул.
Другое дело, что к to_chars_result::ptr можно получить доступ даже если в to_chars_result::ec находится код ошибки… Вот это не есть хорошо. Какой-нибудь std::variant<char*, std::errc> был бы, наверное, уместнее. Но, вероятно, с точки зрения эффективности to_chars_result обходится дешевле.MooNDeaR
13.10.2017 13:42Вот тут меня тож переклинило. Зачем спрашивается std::optional?
eao197
13.10.2017 13:54std::optional не поможет вернуть код ошибки в случае неудачи. А вот аналог Rust-овского Result-а в стандартной библиотеке бы не помешал.
0xd34df00d
13.10.2017 18:56Поверх std::variant можно сделать Result/Either. Даже с монадической структурой (только вместо >>= придётся писать >>, так как >>= правоассоциативен).
Но было бы приятно, если бы это было в стандартной библиотеке, да.JegernOUTT
13.10.2017 23:32Есть интересный proposal как раз на эту тему http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0650r0.pdf
Ждём, верим :)
xFFFF
13.10.2017 11:58Когда уже откажутся от header-файлов?
kosmonaFFFt
13.10.2017 12:13Когда завезут модульную систему + лет 10 (а может и больше) на избавление от легаси.
Idot
13.10.2017 12:33А что в них настолько плохого, что от них нужно обязательно избавиться?
PS Интересно, а что мешает писать их не используя?
ZakharS
13.10.2017 12:36Время компиляции в них плохое. При каждом include парсятся многомегабайтные заголовки библиотек. А как вы предлагаете их не использовать? Везде писать extern?
TargetSan
13.10.2017 12:54Многомегабайтные заголовки сами по себе не такая большая беда. Компиляторы довольно давно научились это дело кешировать. Две гораздо большие проблемы — война макросов и свалка определений.
Война макросов, как можно догадаться — конфликт и наложение эффектов от макросов, определённых в разных заголовках.
Свалка определений — тащим одну маленькую шаблонную функцию, а получаем в область видимости пол-буста вместе с MPL. К тому же, приводит к необходимости тащить вместе с библиотекой заголовки всех её зависимостей (и правильно их раскладывать по местам), которые отметились в заголовках самой библиотеки. И транзитивные в том числе.
xFFFF
13.10.2017 13:18+1Я просто перешел на C#.
yarric
13.10.2017 13:48-1В C# уже завезли RAII и нативную компиляцию?
F0iL
13.10.2017 15:21ru.wikipedia.org/wiki/Ngen
По сути дела, нативная компиляция.yarric
14.10.2017 11:32А насколько он сравним с С++ по эффективности генерируемого кода?
F0iL
15.10.2017 01:10Попробуйте поискать тесты, я не интересовался этим вопросом.
Теоретически, на некоторых задачах даже обгонять может (по аналогии с оптимизирующими JIT-компиляторами — встречал подобные тесты для JVM, например), т.к. бинарь компилируется непосредственно под имеющуюся известную процессорную архитектуру/поколение, следовательно, можно сразу очень эффективно оптимизировать код.FoxCanFly
15.10.2017 14:36Все компиляторы C++ умеют компилировать под имеющуюся известную процессорную архитектуру/поколение
qw1
15.10.2017 15:07В дистрибутивах, где всё собирается из исходников под свою машину, традиционные компиляторы всегда лучше jit.
F0iL
15.10.2017 18:18Само собой. Но хороший профит из этого можно извлечь только при условии, что у вас есть возможность собирать ваш софт непосредственно на машине у конечного пользователя (как принято в некоторых Linux-дистрибутивах), или если программа пишется под конкретный ограниченный круг железа. При дистрибьюции же бинаря «широкой публике», увы, с просторами для оптимизаций все не так прекрасно.
Hazactam
13.10.2017 18:50Когда уже откажутся от header-файлов?
Когда завезут модульную систему
Свалка определений — тащим одну маленькую шаблонную функцию, а получаем в область видимости пол-буста вместе с MPL.
Я просто перешел на C#.
А я — никуда уходил с Delphi, там всего этого 'счастья' не было изначально.alan008
13.10.2017 20:30Поддерживаю, в современных Delphi анонимные функции и обобщенные типы (generics) намного прятнее всех этих templates и deduction rules.
ZakharS
13.10.2017 12:30даже если спецификатор constexpr не указан, лямбда все равно будет constexpr, если это возможно
Зачем же тогда указывать constexpr? Явная декларация о намерениях? Мне кажется, С++ движется в этом смысле в сторону питона — explicit is better than implicit.ZakharS
13.10.2017 12:41Постоянное дежавю с питоном. Похоже, в комитет проникли питонисты
auto[iter, ok] = mySet.insert(42);
Python: a,b=1,2netch80
13.10.2017 21:39Ну вообще-то концепция, по которой у функции всегда предполагалось только одно значение-результат, выглядит устаревшей. Она была хороша в период ранней «математизации» понятия функции (особенно по сравнению с subroutine в Fortran, где функции обязаны были быть «чистыми»), но сейчас нет смысла добровольно вжиматься в прокрустово ложе. Тем более что примеров, когда реально передаётся несколько значений, но все кроме одного идут косвенными каналами — полно в любом системном API.
А с чем сравнивать возврат нескольких значений — с Python, Go, Swift, Erlang, Haskell, чем-то ещё — вопрос персонального опыта. В данном случае второй ok это ближе к тому, что я видел по Go. Был бы он первым — был бы стиль Erlang :)Free_ze
16.10.2017 14:10Чем это принципиально отличается от ссылочных out-параметров?
netch80
16.10.2017 20:41Чтобы ответить на этот вопрос, мне нужно знать уровень Вашей «принципиальности» подхода к этой разнице. Но есть два основных аспекта:
1. Формальный — что это именно части полного результата функции и соответственно не требуют рисования промежуточных переменных.
2. Практический — что в точно такой же манере, как современные ABI предпочитают передавать K первых параметров через регистры, чтобы гонять через RAM — результаты тоже можно передавать через регистры и не заниматься косвенным доступом.
Ну а насколько Вам это будет принципиально — не могу предсказать.
Amomum
13.10.2017 16:40На мой взгляд, минус constexpr в том, что он все равно не гарантирует выполнения на этапе компиляции. Вполне можно было вообще все функции считать constexpr по-умолчанию, а то даже в С++17 много функций из STL не constexpr (в смысле, просто перед ними не приписан constexpr), и их не получается использовать на этапе компиляции.
0xd34df00d
13.10.2017 20:19Это не минус constexpr, это распространённое заблуждение на тему семантики constexpr и зачем оно нужно. constexpr [у функции] нужен не для того, чтобы гарантировать её вычисление во время компиляции, он нужен для того, чтобы компилятор на эту функцию в constexpr-контексте вообще посмотрел.
khim
14.10.2017 01:34Вполне можно было вообще все функции считать constexpr по-умолчанию
Нельзя. По историческим причинам. Функции не описанные какconstexpr
компилятор, в большинстве случаев, в нужном контексте просто не видит (раздельная компиляция, всё такое). Так чтоconstexpr
таки нужен.
Но можно было бы всёinline
-функции сделатьconstexpr
— это правда…Amomum
14.10.2017 01:55Нельзя. По историческим причинам. Функции не описанные как constexpr компилятор, в большинстве случаев, в нужном контексте просто не видит (раздельная компиляция, всё такое).
Не могли бы вы объяснить по-подробнее? Почему прям нельзя?
Допустим, не видит компилятор тела функции из библиотеки — ну и ладно, в рантайме вызовем. В конце концов, constexpr не гарантирует выполнения на этапе компиляции.
Другое дело, что время компиляции от этого, скорее всего, сильно выросло бы.khim
14.10.2017 02:36В конце концов, constexpr не гарантирует выполнения на этапе компиляции.
В случае с описанием переменной (а также использования в качестве параметра типа, размера массива и в других местах) — гарантирует.
Не могли бы вы объяснить по-подробнее? Почему прям нельзя?
Потому что описатьconstexpr
-функцию без тела — нельзя, а безconstexpr
-можно.
Но реально, конечно, это всё поблажки разработчикам компиляторов:constexpr
-функции ведь приходится интерпретировать — а для этого, фактически, отдельный транслятор нужен… Вот и ограничивают их. Вначале ограничения были совсем драконовскими, но сейчас потихоньку гайки отпускают.
andy_p
13.10.2017 16:49> Зачем же тогда указывать constexpr?
Если компилятор не сможет сделать constexpr, он об этом сообщит.khim
14.10.2017 01:39Если компилятор не сможет сделать constexpr, он об этом сообщит.
К сожалению всё не так. Это происходит в любом случае в месте подстановки. Посмотрите сами — вызовprintf
не машает функции отрабатывать в компайл-тайме. Если он не триггерится, конечно.
Ну так и нафига козе баян — в смысле нафига явно тут указывать, что функцияconstexpr
? Если возможность её использовать всё равно зависит от конкретного значения?
P.S. Кстати за счётconstexpr
любой коспилятор C++ — теперь где-то ещё и инетерпретатор. clang отрабатывает раз в 8 быстрее, чем gcc…
Gizmich
13.10.2017 12:42Здорово что они дополняют std простыми и полезными функциями. Но вот…
Свертка параметров шаблона (Fold expressions)
Сложность понимания кода все еще очень высока. Все еще нужно разбираться в нюансах, а не читать «на лету» кодEgorBredikhin Автор
13.10.2017 12:42+3Fold expressions, как раз уменьшают сложность работы с variadic templates, ведь код получается куда нагляднее чем с использованием рекурсии.
ooki2day
13.10.2017 12:46std::map<int, std::string> myMap; .... if (auto[it, ok] = myMap.insert({ 2, "hello" }); ok) { .... }
Не могу понять зачем это надо в конкретном примере. Чем хуже сделать insert до if, а в if просто проверить ok?ZakharS
13.10.2017 12:48Это больше синтаксический сахар — не нужна лишняя строка. И да, выше указали более существенное преимущество про сужение scope
ZakharS
13.10.2017 12:53for (const auto &[key, value] : myMap)
Наконец-то не надо писать iter->first, iter->second!
Mike255
13.10.2017 12:54Интересно. А настанет ли момент, когда из языка будут удалять ненужные элементы, а не добавлять новые?
yarric
13.10.2017 13:50Просто не используйте ненужные элементы.
Mike255
13.10.2017 15:37+1Другие-то их используют, в библиотеках, например. Скоро найти человека, который знает весь синтаксис С++ будет нереально. И это меня печалит.
Idot
13.10.2017 18:23+2Потому всё это напоминает мне PL/1, когда обычный программист не знал большей части языка, из-за чего возникали реальные проблемы с пониманием чужого кода написанного человеком, знающим другие части языка.
yarric
14.10.2017 14:11Ну библиотеки не всем надо читать в обычном случае. А так есть же немало стандартов кода и линтеров, помогающих их придерживаться.
alexeykuzmin0
13.10.2017 15:39Так вроде std::auto_ptr удалили.
Mike255
13.10.2017 15:42-2Когда случайно узнал — радовался неделю. Теперь жду когда же удалят iostream и все что с этим связано. Ну и половину нововведений со времен C++11.
mayorovp
13.10.2017 15:55А с iostream-то что не так? Переделать его, возможно, и правда следует. Но удалять?..
Mike255
13.10.2017 15:59У меня стойкое чувство, что iostream (и друзья) спроектированы по двум причинам: оправдать наличие в языке виртуальных классов (ромб смерти) и показать как здорово можно переопределять операторы (в данном случае << и >>). А для того чтобы показать на примере специализацию шаблонов специализирован класс std::vector. И т.д. и т.п.
mayorovp
13.10.2017 16:18+2Эти классы взялись не на пустом месте, а были сделаны как абстракция понятия "поток данных". Во всех современных языках есть свои аналогичные абстракции:
Delphi 7 — TStream
Java — java.io.InputStream, java.io.OutputStream
C# — System.IO.Stream
Python — io.IOBase
Node.js — модуль stream
Поэтому без замены iostream на что-то более красивое убирать его из стандартной библиотеки нельзя.
Mike255
13.10.2017 16:29+1Полностью согласен. Посмотрите на грамотные java.io.InputStream, java.io.OutputStream. А вот зачем было ввод и вывод мандить в std::ios? И зачем были нужны операторы << и >>. В реальных проектах они неудобны, сложно настраивать форматирование, невозможно делать локализацию и т.д. printf по сравнению с ними просто сказка.
eao197
13.10.2017 17:20+2и т.д. printf по сравнению с ними просто сказка.
Ровно до тех пор, пока не придется использовать printf в обобщенном коде. Или до тех пор, пока не придется использовать printf для вывода в определенный пользователем поток данных.
Для тех, кто исходит на известную субстанцию от iostreams, уже давно есть fmtlib. Которая, среди прочего, позволяет работать с пользовательскими типами, для которых определены операторы сдвига именно в std::ostream.
TargetSan
13.10.2017 16:42+1Я думаю, имеется ввиду выкинуть
iostreams
на мороз именно в текущем виде. Просто ради примера, в Rust абстракции байтовых потоков ввода-вывода требуют реализации по одной функции на чтение и запись, соответственно. Это в самой базовой форме. В С++ тщательно ковыряться с буфером потока и, возможно, самим классом потока.
DaylightIsBurning
13.10.2017 17:00а конкретней, какие именно новые фичи нужно удалить?
Mike255
13.10.2017 17:10Ну списка я не веду, дабы не расстраиваться. Для примера Вам новая инициализация через { и }.
DaylightIsBurning
13.10.2017 17:17+2А чем она хуже того, что было ранее, кроме того что нужно сесть и разобраться в том, как она работает? C++ — expert friendly. Одна из идей языка — не жертвовать гибкостью в пользу простоты изучения. Есть языки с противоположной парадигмой — golang, например.
Конечно, в С++ есть места, где сложность получается избыточной и не несёт полезной нагрузки — с ними пытаются бороться.
Я не говорю, что golang — хуже, просто это другой язык с другой установкой, идеей и целями. Не нужно все языки под golang причёсывать.Mike255
13.10.2017 21:28Насчет { } немного долгое объяснение. Когда-то очень давно, когда в C++ вводились классы их синтаксически приравняли к структурам. Но в моей реальной практике (и не только в моей) слово class используют, когда нужен полноценный объект, а слово struct, когда нужны только переменные члены класса (и они все открытые). В struct практически не пишутся функции, иногда пишется конструктор, еще реже деструктор. И вот именно дефолтного конструктора по всем членам структуры мне всегда не хватало. Скобки { }
решают это проблему, но заодно вводят путаницу в вызовы конструкторов. По мне, так правильней было бы разделить назначение struct и class. И как минимум сделать дефолтный конструктор для struct по списку членов.DaylightIsBurning
13.10.2017 21:46Ну то есть по Вашему мнению тот факт, что {} инициализация могла бы быть сделана ещё лучше означает, что её нужно совсем удалить? Да и Ваш вариант обладает недостатком нарушения совместимости с C++98, так что его превосходство крайне спорно. Новых еще лучших несовместимых языков хватает — D, Rust, golang,… У C++ другие цели, при этом преимуществ более новых языков никто не отрицает.
Mike255
13.10.2017 21:54Два крайних подхода. 1. Если что-то можно сделать в языке без ввода новых конструкций, то новую конструкцию не вводят. 2. Вводить новую конструкцию по любому мелочному поводу.
Истина где-то посередине. Но середина у всех разная. У меня она ближе к пункту 1.
Про разделения struct и class я говорил (увы не уточнил) в контексте, как надо было сделать, когда вводились классы. Сейчас, понятно, это сделать уже нельзя.
third112
13.10.2017 15:49+1Особенно понравился [[maybe_unused]]! Интересно, а скоро будет [[maybe_wrong]]?
DaylightIsBurning
13.10.2017 17:11Совершенно неуместная ирония. Вы хорошо знакомы с C++? Встречали паттерн
(void)param2;
? Это ему более удобная замена. Кроме условной компиляции такое бывает необходимо при перегрузке виртуальных функций, мб ещё где…third112
13.10.2017 22:11Да. Увы. Знаком. Перманентно перевожу с С++ на Delphi-7 (а еще CUDA — там проще без Delphi), всякие паттерны дополнительно затрудняют перевод. С++ может и великий, но не единственный язык, поэтому ИМХО стоит больше думать о сосуществовании с другими языками.
DaylightIsBurning
13.10.2017 22:15И как это связанно с [[maybe_unused]]? Что в нём плохого? Это же просто подсказка программисту от компилятора типа тех которые выдает статический анализатор.
C++ и так сделал всё необходимое для сосуществования — обертки над C++ доступны практически во всех языках.third112
13.10.2017 23:02Я не говорил, что в [[maybe_unused]] что-то плохое. И уж точно не собирался устраивать холивар «какой язык лучше», я только хотел сказать, что ИМХО слишком много стало архитектурных излишеств, как было сказано выше:
Скоро найти человека, который знает весь синтаксис С++ будет нереально. И это меня печалит.
DaylightIsBurning
13.10.2017 23:11А почему именно эта конструкция излишняя? При чем тут архитектура? что значит «излишняя» для вас? Так-то до ассемблера можно дойти. И даже дальше.
third112
13.10.2017 23:44На мой взгляд, эта конструкция одна из наиболее выразительных, где избыточность выражена через maybe. Архитектура тут при том, что было постановление.
что значит «излишняя» для вас?
Думаю, очевидно, что для меня как переводчика это лишняя заморочка.
Так-то до ассемблера можно дойти.
А что Вы имеете против ассемблера?
Вспомнил, нпр., такое утверждение:
Есть задачи, которые компилятор на языке высокого уровня решить лучше человека не сможет, по крайней мере пока, поэтому да смогу, но пример выбирать не вам. Доказывать очевидное не буду, лучше возьмите открытые кодеки и системы распознавания образов — думаете люди от нечего делать там целые функции на ассемблере или интрисиками пишут?
Далее:
И даже дальше.
А куда дальше ассемблера?
DaylightIsBurning
14.10.2017 00:14Какое это имеет отношение к вопросу «почему именно эта конструкция излишняя?».
конструкция одна из наиболее выразительных, где избыточность выражена через maybe
то есть она все таки не излишняя?
А настанет ли момент, когда из языка будут удалять ненужные элементы
…
Особенно понравился [[maybe_unused]]!
… слишком много стало архитектурных излишеств
то есть, очевидно, слово «понравился» имело ироничный характер.
для меня как переводчика это лишняя заморочка.
Во-первых даже для «переводчиков» эта заморочка совершенно не лишняя т.к. показывает намерение о том, что эта переменная на самом деле, вероятно, не нужна и её можно не переносить. Во-вторых, мне кажется, очевидно, что интересы переводчиков с С++ при разработке стандарта имеют крайне низкий приоритет. Гораздо важнее удобство C++ разработки.
А что Вы имеете против ассемблера?
Вы специально из контекста выдёргиваете? Там речь шла о том, что ассемблер — плохой C++ (и наоборот, но речь не об этом). Соответсвенно, «излишества» — понятие относительное. И снова возвращаемся к вопросу, почему Вы считаете, что конкретно "[[maybe_unused]]" — излишество, причём самое явное в новом стандарте.
А приводить в качестве аргумента об архитектуре ЯП постановление ЦК КПСС по архитектуре зданий от 1955 года — это вообще какая-то самодискредитация. Во-первых не та предметная область. Во-вторых в строительстве нет единого понимания о том, что такое хорошо, но постановления ЦК КПСС тут точно не авторитет, также как и по отношению к генетике и кибернетике. И в третьих последствия этого решения всем нам печально известны в виде страшных как смертный грех жилых районов в городах СНГ, которые не критиковал только ленивый. Примеров того, как от этой «архитектуры» старались и стараются избавиться, везде, где представляется такая возможность — валом.third112
14.10.2017 01:31А приводить в качестве аргумента об архитектуре ЯП постановление ЦК КПСС по архитектуре зданий от 1955 года
Я сказал "архитектура", а не «архитектура ЯП»! И не приводил в качестве аргумента, а только в качестве сравнения, метафоры.
очевидно, что интересы переводчиков с С++ при разработке стандарта имеют крайне низкий приоритет.
Именно это я и говорил:
стоит больше думать о сосуществовании с другими языками
И возможно ли доказать, что удобство C++ разработки повысилось?:
Гораздо важнее удобство C++ разработки.
DaylightIsBurning
13.10.2017 16:56Так два года назад начали:
«Within C++ is a smaller, simpler, safer language struggling to get out.» — Bjarne Stroustrup
TargetSan
13.10.2017 16:59«Within C++ is a smaller, simpler, safer language losing struggle to get out.»
Fixed
DaylightIsBurning
13.10.2017 17:06Ещё рано судить, мне кажется. Я готов дать Core Guidelines шанс.
TargetSan
13.10.2017 17:19Увы, я потихоньку разувериваюсь. Некоторые хронические болячки либо не решаются, либо решаются с адскими задержками. Зато накидывают всякой ерунды — вроде зета-функции Римана. Вот самое место в стандарте! Проблема же миграции на другие языки часто в том, что С++ несовместим ни с кем кроме С++ — причём часто только своим диалектом.
Dima_Sharihin
14.10.2017 12:31С++ несовместим ни с кем кроме С++
C++ совместим (хотя бы частично) с С, а это в эмбеддеде уже огромный плюс. Вот кто точно почти не совместим ни с кем, кроме себя — это стандартный .NET, который представляет собой вещь в себе и приносящий дикие боли при попытке подружить что-либо с нативной библиотекой (писать кучу [DllImport] та еще романтика, буэ)
mayorovp
14.10.2017 14:17+1Для сложных случаев в .NET существуют аж две альтернативы DllImport — COM и C++/CLI
Dima_Sharihin
14.10.2017 15:15+1И обе работают только в пределах MS Windows?
Но дотнет еще ничего, я боюсь подумать про Node.js и прочие "новомодные" фреймворки.
mayorovp
14.10.2017 16:27Да нет, вроде бы и в линуксе есть… Хотя сам я не проверял как оно там работает.
splav_asv
14.10.2017 21:07Но дотнет еще ничего, я боюсь подумать про Node.js и прочие «новомодные» фреймворки.
Обёртки писать, как ещё. Для Python много всего понаписано. Для node.js тоже статьи попадались — видимо как-то можно.
encyclopedist
15.10.2017 00:45Эти функции были добавлены в C++ для совместимости с C, где они появились в C99.
yarric
14.10.2017 14:13Интересно, есть ли утилита или файл конфигурации для линтера, чтобы обеспечить строгое следование этим гайдам?
ZakharS
13.10.2017 13:58Насчет параллельных алгоритмов — один из участников CppCon активно критиковал то, что этот функционал слишком легко доступен для новичков. Надо всего лишь добавить один параметр. Такая простота дает новые возможности отстрелить не только ногу :)
By pretending that parallelisation is simple – it has an enormous potential for unsuspecting developer trying to use it – and getting the whole project badly burned
Детали здесьDaylightIsBurning
13.10.2017 17:02+1Так C++ всегда был expert friendly, новичкам ничего не обещали :)
hdfan2
14.10.2017 15:32С++ — очень дружелюбный язык. Проблема в том, что он сам выбирает, с кем ему дружить.
ZakharS
13.10.2017 14:05Большое спасибо за прекрасный обзор и, главное, ясные примеры! Вы решили последовать совету Страуструпа на CppCon 2017 :) Он сказал, что самое главное — это не фичи, а примеры их правильного применения.
Вообще, новый стандарт никак не минорный, каким был С++14. И хотя многое из списка выглядит как синтаксический сахар — все очень востребовано и поможет заметно улучшить читаемость и не писать лишний код. В который раз убеждаюсь, что в комитете сидят очень адекватные люди. А всеми ожидаемые Networking и Modules задерживают не просто так — там слишком много всего надо учесть, чтобы потом не переделывать и не ломать совместимость.
Xandrmoro
13.10.2017 14:36С одной стороны, много клёвых удобных фич, с другой — все дальше и дальше write-only…
Petrenuk
13.10.2017 15:42Чёрт возьми, а я и не знал, что столько всего полезного всё-таки попало в стандарт! Декомпозиция через auto, свёртки для variadic templates — это очень прикольно. Надо начинать использовать)
Viacheslav01
13.10.2017 17:29+3Во что превратился С++? Есть люди не из суперов заседающих в комитете, кто знает все детали языка?
DaylightIsBurning
13.10.2017 22:07Это сродни жалоб на то, что не все знают русский на уровне профессора фил-фака МГУ, а профессор МГУ не понимает профессиональных жаргонов.
Вообще эта проблема сущесествует уже давно, шаблонная магия неспроста называется магией. К счастью, она обычно изолированна в тех местах, куда большинство разработчиков почти не заглядывает.
Нельзя создать тривиальный для понимания и при этом универсальный (гибкий) язык, коим старается быть C++. К этому идеалу можно стремится, и C++-next + Core Guidelines как раз в этом направлении и двигаются.third112
14.10.2017 01:42универсальный (гибкий) язык
универсальный = гибкий?
Самый универсальный ЯП — это ассемблер: остальные языки на него транслируются и составляют подмножества комбинаций ассемблерных инструкций. При этом не все возможные комбинации задействованы.qw1
14.10.2017 09:10+1Нет такого языка, как «Ассемблер», а есть языки «Ассемблер ARMv8», «Ассемблер x86» последний ещё и с разными синтаксисами (Intel, AT&T) и диалектами (i386, i686, SSE2).
И который из этих ассемблеров универсальный?third112
14.10.2017 14:32Каждый универсальный для своей платформы.
qw1
14.10.2017 14:54Почему тогда нельзя сказать: каждый язык программирования универсальный… для своего класса задач. Не теряется ли суть понятия «универсальный»?
third112
14.10.2017 15:05Есть языки, которые подходят для широкого круга задач — их называют универсальными, а есть для узкого круга.
qw1
14.10.2017 15:16Так и ассемблер работает на одной платформе, а не на широком круге.
third112
14.10.2017 15:31Я не про платформы, а про задачи. Язык запросов SQL, нпр., реализован на многих платформах, но не является универсальным.
qw1
14.10.2017 16:36Платформу выбирают под задачу. Из требований к задаче вытекает, на какой платформе она будет решаться. Таким образом язык, привязанный к платформе, заведомо проигрывает в универсальности на всём множестве задач.
third112
14.10.2017 17:23Платформу выбирают под задачу.
Кто выбирает? Я не выбираю. Сейчас все задачи решаю на Intel Core i7, а было время, когда решал на ЕС-1022. А если серьезно, то существуют эмуляторы (см. вики) — можно любой ассемблер на любой достаточно мощной машине воспроизвести. Универсальность — свойство, заложенное в языке, а не ассортимент реализаций. Нпр., нет Дельфи-7 для CUDA, но возможно реализовать.qw1
14.10.2017 20:03Кто выбирает? Я не выбираю
Значит, узкий спектр задач. Например, игру типа Pokemon GO логично делать на мобильных платформах, а не на Core-i7.
А если серьезно, то существуют эмуляторы (см. вики) — можно любой ассемблер на любой достаточно мощной машине воспроизвести
А что толку, если платформа (в более широком смысле, чем аппаратная) вам не позволит писать на ассемблере. Например, если требуется сделать клиентскую логику на веб-странице.third112
14.10.2017 20:20Значит, узкий спектр задач. Например, игру типа Pokemon GO логично делать на мобильных платформах, а не на Core-i7.
ИМХО Pokemon GO очень специфичная задача. Но вроде QEMU поможет сделать ее на Core-i7.
qw1
14.10.2017 20:10Универсальность — свойство, заложенное в языке, а не ассортимент реализаций
Ассемблер для ЕС-1022 и для Core-i7 — один язык, или разные?third112
14.10.2017 20:22разные.
qw1
15.10.2017 12:51И оба — универсальные?
third112
15.10.2017 13:45Да.
qw1
15.10.2017 15:04+1Я вас понял, особенно после замечания про QEMU. Универсальность вы рассматриваете с математической, а не практической стороны. Ну, как машину Тьюринга.
В таком случае, ассемблеры не являются чем-то особенным, любой полный по Тьюрингу язык можно считать универсальным. Тот же javascript — пишем на нём эмулятор x86, ставим windows, qemu, запускаем эмулятор андроида — задача выполнена.third112
15.10.2017 16:18Согласен. В начале ветки я спросил:
универсальный = гибкий?
Изменю утверждение:
Самыйуниверсальныйгибкий ЯП — это ассемблер: остальные языки на него транслируются и составляют подмножества комбинаций ассемблерных инструкций. При этом не все возможные комбинации задействованы.qw1
15.10.2017 16:37Я понимаю слово «гибкий» как «адаптирующийся к изменениям».
То есть язык, который с одной стороны развивается в соответствии с веяниями программисткой моды (как c#), с другой стороны, позволяет с минимальными усилиями модифицировать программы под новые требования (как специализированные DSL).
В этом смысле, ассемблеры — наиболее «жёсткие» языки.third112
15.10.2017 17:07Зависит от определения. Я понимаю слово «гибкий» как возможность по-разному решить задачу. Давно не возникало таких задач и не знаю как сейчас, когда компиляторы очень хорошо оптимизируют, но раньше удавалось получать ускорение переписывая критические участки на ассемблере.
Andrey_Epifantsev
13.10.2017 17:48Удаленные возможности
Как же они на это пошли? А как же священная обратная совместимость со старым кодом и с C?alexeykuzmin0
13.10.2017 18:00Ну так-то удаленные фичи были как минимум со времен C++11.
Idot
13.10.2017 19:46А можно познакомиться со списком удалённого?
PS по Вашим словам, похоже практики настолько мало знакомы с нововведениями, что не успевают даже заметить и прочувствовать, что что-то добавили и удалили.
khim
14.10.2017 01:50А можно познакомиться со списком удалённого?
Приложение С в стандарте этому посвящено.
PS по Вашим словам, похоже практики настолько мало знакомы с нововведениями, что не успевают даже заметить и прочувствовать, что что-то добавили и удалили.
Не совсем так. В C++11 удалили какие-то фичи, которые были обьявлены как «устаревшие» в C++03, в C++17 — то, что было обьявлено устаревшим в C++11.
export template
удалили, который ни одним компилятором не поддерживался (хотя вру — вроде один экспериментальный всё же был).
Deamon87
13.10.2017 22:35-1Это вы еще не видели brace инициализаторов в сочетании со структурами
this->exteriorPortals.push_back({ groupId: i, portalIndex : -1, portalVertices: {}, frustumPlanes: frustumPlanes, level : 0 });
Вот это сейчас совершенно легальный код в С++. Привет JS?khim
14.10.2017 01:51Это не валидный код в C++. Это всго лишь пропозал для C++20, причём в том виде, как вы его описали — это всё равно ошибка.
И да — это весьма полезная фича, благо она есть в C99, активно используется во многих проектах, а в режиме C++ поддерживается, например, clang'ом.Deamon87
14.10.2017 16:33Ну не знаю. Я могу предположить, что это самодеятельность разработчиков компилятора, но как минимум gcc 6 съедает такой код и не давится.
Использование этой конструкции помогло мне очень эффективно портировать код с JS на C++.
khim
14.10.2017 23:11Я могу предположить, что это самодеятельность разработчиков компилятора, но как минимум gcc 6 съедает такой код и не давится.
Прекрасно. В таком случае дать ссылку на https://godbolt.org/ вас, разумеется не затруднит. Дополните ваш пример до компилируемого кода — и можно будет что-то обсуждать.NeonMercury
14.10.2017 23:35Вот, если я всё правильно понял: godbolt.org/g/ZSLJgN
Если что, не я автор изначального комментария, но мне стало интересно и я проверил.khim
15.10.2017 00:37Ого. Не ожидал. Clang тоже сьедает, кстати — но по крайней мере warning'и выдаёт.
Это GNU'сное расширение, которое в стандарт C99 (а теперь и C++20) решили не вносить. Было принято решение использовать другой синтаксис, никакого отношения к JavaScript'у не имеющий.
То есть в GCC реализовали инициализаторы с одной стороны криво и не полностью, а с другой — со своими собственными расширениями.
Прэлестно, просто прэлестно.NeonMercury
15.10.2017 15:26Да, я так же засомневался, что это стандартное поведение, но стандарт изучить не успел. Но так же подумал, что это один из вариантов инициализации через точку:
struct A { int x; int y; int z; }; A a{.y = 2, .x = 1}; A b{.x = 1, .z = 2};
f1inx
16.10.2017 12:45Такие инициализаторы были доступны в GCC еще 20лет назад, а после выхода C99 они считаются deprecate и теперь нужно использовать C99 синтаксис.
С чего вы взяли что они реализованы криво и не полностью?
Если вы не знали что 90-99% новых стандартов C и C++ сначала обкатываются на GCC то это тоже, просто прэлестно. И «как ни странно» рано или поздно высокий процент расширений GCC попадают в стандарт.
Cupper
14.10.2017 02:55Constexpr if
…
До C++17 нам пришлось бы использовать SFINAE и enable_if:
Не понял, зачем?
template <typename T> T GetValue(T t) { return t; } template <typename T> T GetValue(T *t) { return *t; } int main() { int i = 5; std::cout << GetValue(i) << std::endl; std::cout << GetValue(&i) << std::endl; }
EgorBredikhin Автор
14.10.2017 16:47В общем случае такой подход не подойдет. Например для
std::is_class
илиstd::is_arithmetic
.
NYMEZIDE
dmpas
это ведь не случайно, что у вас написано кириллицей? Импортозамещение и все дела?
qw1
Ну, C# позволяет писать идентификаторы любыми символами, которые в стандарте unicode определены как Letter, почему бы в C++ не принять эту фичу.
netch80
Тут возникает ряд существенных граблей.
Например, обязан ли язык различать A (U+00C1) и A? (U+0041 U+0301)? Python различает, но приведение всех идентификаторов к NKFC может быть дороговато (в компилятор втягивать что-то размера ICU, причём на этапе, где лексер и так отрабатывает много специфики языка — см. например Clang — где есть лексический парсинг обычного кода, строк препроцессора, кода внутри блока `#if 0`, и т.п.) А вот Go ничего не делает с этим — и можно получить злобные проблемы от внешне одинаковых идентификаторов в разных местах, так, что редактор этого не покажет.
А когда апдейтится стандарт Unicode — менять таблицы вслед?
Требовать ли от внешних средств типа линкера поддержки таких идентификаторов, и что делать, если не поддерживает?
Я не согласен со многими решениями в C++, но сама идея, что это (вместе с C) всё-таки языки системного программирования, предполагает, что выход за пределы базовых доступных везде средств должен выполняться очень ограниченно и осмотрительно.
Ведь даже наличие ASCII, считаем, стало обязательным только с C++17 — я имею в виду отказ от триграфов, которые применялись для возможности написания на C++ в странных местах типа zSeries с локализованными вариантами EBCDIC…
khim
В случае с C++ всё просто: ничего никуда не конвертируется. В стандарте просто сказано: An identifier is an arbitrarily long sequence of letters and digits. Each universal-character-name in an identifier shall designate a character whose encoding in ISO 10646 falls into one of the ranges specified in E.1.
И компиляторы, хотя и не все, давно позволяют называет идентификаторы по русски. Это вопрос к программистам, а не к стандарту сегодня, на самом деле.
khim
SvyatoslavMC
1С++17?