- PVSM.RU - https://www.pvsm.ru -
Мы поговорим о восьми удобных изменениях, которые влияют на ваш повседневный код. Четыре изменения касаются самого языка, а ещё четыре — его стандартной библиотеки.
Вам также может быть интересна статья Десять возможностей C++11, которые должен использовать каждый C++ разработчик [1]
Некоторые примеры я брал из докладов на конференциях Russian C++ User Group [2] — за это огромное спасибо её организаторам и докладчикам! Я брал примеры из:
Удобно декомпозировать std::pair
, std::tuple
и структуры с помощью нового синтаксиса:
#include <string>
struct BookInfo
{
std::string title; // In UTF-8
int yearPublished = 0;
};
BookInfo readBookInfo();
int main()
{
// Раскладываем поля структуры на переменные title и year, тип которых выведен автоматически
auto [title, year] = readBookInfo();
}
В C++17 есть ограничения декомпозиции при объявлении:
auto [title, [header, content]] = ...
Декомпозиция при объявлении в принципе может раскладывать любой класс — достаточно один раз написать подсказку путём специализации tuple_element
, tuple_size
и get
. Подробнее читайте в статье Adding C++17 structured bindings support to your classes (blog.tartanllama.xyz) [5]
Декомпозиция при объявлении хорошо работает в контейнерах std::map<>
и std::unordered_map<>
со старым методом .insert()
и двумя новыми методами :
Пример декомпозиции с try_emplace и декомпозиции key-value при обходе map:
#include <string>
#include <map>
#include <cassert>
#include <iostream>
int main()
{
std::map<std::string, std::string> map;
auto [iterator1, succeed1] = map.try_emplace("key", "abc");
auto [iterator2, succeed2] = map.try_emplace("key", "cde");
auto [iterator3, succeed3] = map.try_emplace("another_key", "cde");
assert(succeed1);
assert(!succeed2);
assert(succeed3);
// Вы можете раскладывать key и value прямо в range-based for
for (auto&& [key, value] : map)
{
std::cout << key << ": " << value << "n";
}
}
Ключевые правила:
std::make_pair
больше не нужны: смело пишите выражения std::pair{10, "hello"s}
, компилятор сам выведет типstd::lock_guard<std::mutex> guard(mutex);
станут короче: std::lock_guard guard(mutex);
std::make_unique
и std::make_shared
по-прежнему нужныВы можете создавать свои подсказки для автоматического вывода параметров шаблона: см. Automatic_deduction_guides [8]
Интересная особенность: конструктор из initializer_list<>
пропускается для списка из одного элемента. Для некоторых JSON библиотек (таких как json_spirit) это может оказаться фатальным. Не играйтесь с рекурсивными типами и контейнерами STL!
#include <vector>
#include <type_traits>
#include <cassert>
int main()
{
std::vector v{std::vector{1, 2}};
// Это vector<int>, а не vector<vector<int>>
static_assert(std::is_same_v<std::vector<int>, decltype(v)>);
// Размер равен двум
assert(v.size() == 2);
}
Избегайте вложенности пространств имён, а если не избежать, то объявляйте их так:
namespace product::account::details
{
// ...ваши классы и функции...
}
Ключевые правила:
[[fallthrough]]
, либо инструкцией break;
[[nodiscard]]
для функций, возвращающих код ошибки или владеющий указатель (неважно, умный или нет)[[maybe_unused]]
для переменных, которые нужны только для проверки в assertБолее подробно об атрибутах рассказано в статье Как пользоваться атрибутами из C++17 [9]. Здесь будут краткие выдержки.
В C++ приходится добавлять break после каждого case в конструкции switch, и об этом легко забыть даже опытному разработчику. На помощь приходит атрибут fallthrough, который можно приклеить к пустой инструкции. Фактически атрибут приклеивается к case, следующему за пустой инструкцией.
enum class option { A, B, C };
void choice(option value)
{
switch (value)
{
case option::A:
// ...
case option::B: // warning: unannotated fall-through between
// switch labels
// ...
[[fallthrough]];
case option::C: // no warning
// ...
break;
}
}
Чтобы воспользоваться преимуществами атрибута, в GCC и Clang следует включит предупреждение -Wimplicit-fallthrough
. После включения этой опции каждый case, не имеющий атрибута fallthrough, будет порождать предупреждение.
В проектах с высокими требованиями к производительности могут практиковать отказ от выброса исключений (по крайней мере в некоторых компонентах). В таких случаях об ошибке выполнения операции сообщает код возврата, возвращённый из функции. Однако, очень легко забыть проверить этот код.
[[nodiscard]] std::unique_ptr<Bitmap> LoadArrowBitmap() { /* ... */ }
void foo()
{
// warning: ignoring return value of function declared
// with warn_unused_result attribute
LoadArrowBitmap();
}
Если вы используете, например, свой класс ошибок, то вы можете указать атрибут единожды в его объявлении.
class [[nodiscard]] error_code { /* ... */ };
error_code bar();
void foo()
{
// warning: ignoring return value of function declared
// with warn_unused_result attribute
bar();
}
Иногда программисты создают переменную, используемую только в отладочной версии для хранения кода ошибки вызванной функции. Возможно, это просто ошибка дизайна кода, и возвращаемое значение следовало обрабатывать всегда. Тем не менее:
// ! старый код!
auto result = DoSystemCall();
(void)result; // гасим предупреждение об unused variable
assert(result >= 0);
// современный код
[[maybe_unused]] auto result = DoSystemCall();
assert(result >= 0);
Правила:
const string&
старайтесь принимать невладеющий string_view
по значению
string
, как и раньшеПодробнее о том, почему string_view лучше всего применять только для параметров, читайте в статье std::string_view конструируется из временных экземпляров строк [10]
Класс string_view
хорош тем, что он легко конструируется и из std::string
и из const char*
без дополнительного выделения памяти. А ещё он имеет поддержку constexpr и повторяет интерфейс std::string. Но есть минус: для string_view
не гарантируется наличие нулевого символа на конце.
Применение optional<>
и variant<>
настолько широко, что я даже не буду пытаться полностью описать их в этой статье. Ключевые правила:
optional<T>
вместо unique_ptr<T>
для композиции объекта T, время жизни которого короче времени жизни владельца
unique_ptr<Impl>
, потому что определение Impl скрыто в файле реализации классаoptional
для обработки ошибок: он не несёт никакой информации об ошибке
Expected<Value, Error>
, основанный на boost::variant<...>
Пример кода с optional:
// nullopt - это специальное значение типа nullopt_t, которое сбрасывает
// значение optional (аналогично nullptr для указателей)
std::optional<int> optValue = std::nullopt;
// ... инициализируем optValue ...
// забираем либо значение, либо -1
const int valueOrFallback = optValue.value_or(-1);
operator*
и operator->
, а также удобный метод .value_or(const T &defaultValue)
operator*
, бросает исключение std::bad_optional_access
при отсутствии значенияstd::nullopt
меньше любого допустимого значенияПример кода с variant: здесь мы используем variant для хранения одного из нескольких состояний в случае, когда разные состояния могут иметь разные данные
struct AnonymousUserState
{
};
struct TrialUserState
{
std::string userId;
std::string username;
};
struct SubscribedUserState
{
std::string userId;
std::string username;
Timestamp expirationDate;
LicenseType licenceType;
};
using UserState = std::variant<
AnonymousUserState,
TrialUserState,
SubscribedUserState
>;
Преимущество variant в его подходе к управлению памяти: данные хранятся в полях значения типа variant без дополнительных выделений памяти. Это делает размер типа variant зависимым от типов, входящих в его состав. Так может выглядеть таблица размеров на 32-битных процессорах (но это неточно):
std::vector<>
&text[0]
, но оно имеет неопределённое поведение на пустых строкахМожет быть, для манипуляций с байтами лучше опираться на библиотеку GSL (C++ Core Guidelines Support Library).
Ключевые правила:
std::filesystem::path
вместо строк во всех параметрах, в которых подразумевается путьcanonical
: возможно, вы имели ввиду метод lexically_normal [14]
relative
: возможно, вы имели ввиду lexically_relative [14]Чем плох boost::filesystem? Оказывается, у него есть несколько проблем дизайна:
Любой опытный программист знает о разнице в обработке путей между Windows и UNIX-системами:
Конечно же filesystem абстрагируется от подобных различий и позволяет легко работать как с платформо-зависимыми строками, так и с универсальным UTF-8:
std::filesystem::path
из std::string
— на Windows конструктор считает входной кодировкой кодировку ОС!Функция std::clamp [19] дополняет функции min и max. Она обрезает значение и сверху, и снизу. Аналогичная функция boost::clamp
доступна в более ранних версиях C++.
Правило "не переизобретайте clamp" можно обобщить: в любом крупном проекте избегайте дублирования маленьких функции и выражений для округлений, обрезаний значений и т.п. — просто один раз добавьте это в свою библиотеку.
Аналогичное правило работает для задач обработки строк. У вас есть своя маленькая библиотека для строк и парсинга? В ней есть парсинг или форматирование чисел? Если есть, замените свою реализацию на вызовы to_chars [20] и from_chars [21]
Функции to_chars
и from_chars
поддерживают обработку ошибок. Они возвращают по два значения:
char*
или const char*
соответственно и указывает на первый code unit (т.е. char или wchar_t), который не удалось обработатьstd::error_code
и сообщает подробную информацию об ошибке, пригодную для выброса исключения std::system_errorПоскольку в прикладном коде способ реакции на ошибку может различаться, следует помещать вызовы to_chars и from_chars внутрь своих библиотек и утилитных классов.
#include <utility>
// конвертирует строку в число, в случае ошибки возвращает 0
// (в отличии от atoi, у которого местами есть неопределённое поведение)
template<class T>
T atoi_17(std::string_view str)
{
T res{};
std::from_chars(str.data(), str.data() + str.size(), res);
return res;
}
Автор: sergey_shambir
Источник [22]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-3/269604
Ссылки в тексте:
[1] Десять возможностей C++11, которые должен использовать каждый C++ разработчик: https://habrahabr.ru/post/182920/
[2] Russian C++ User Group: http://cpp-russia.ru/
[3] Антон Полухин. C++17 (C++ SIBERIA 2016): http://cpp-russia.ru/?page_id=1253
[4] Александр Фокин. C++17, который мы заслужили (C++ SIBERIA 2017): https://www.youtube.com/watch?v=I0oerm0MYQY
[5] Adding C++17 structured bindings support to your classes (blog.tartanllama.xyz): https://blog.tartanllama.xyz/structured-bindings/
[6] try_emplace: http://en.cppreference.com/w/cpp/container/map/try_emplace
[7] insert_or_assign: http://en.cppreference.com/w/cpp/container/map/insert_or_assign
[8] Automatic_deduction_guides: http://en.cppreference.com/w/cpp/language/class_template_argument_deduction#Automatic_deduction_guides
[9] Как пользоваться атрибутами из C++17: https://medium.com/@sshambir/%D0%BA%D0%B0%D0%BA-%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%D1%81%D1%8F-%D0%B0%D1%82%D1%80%D0%B8%D0%B1%D1%83%D1%82%D0%B0%D0%BC%D0%B8-%D0%B8%D0%B7-c-17-622a6b53225a
[10] std::string_view конструируется из временных экземпляров строк: https://medium.com/@sshambir/std-string-view-%D0%BA%D0%BE%D0%BD%D1%81%D1%82%D1%80%D1%83%D0%B8%D1%80%D1%83%D0%B5%D1%82%D1%81%D1%8F-%D0%B8%D0%B7-%D0%B2%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%BD%D1%8B%D1%85-%D1%8D%D0%BA%D0%B7%D0%B5%D0%BC%D0%BF%D0%BB%D1%8F%D1%80%D0%BE%D0%B2-%D1%81%D1%82%D1%80%D0%BE%D0%BA-3453e7d3a75b
[11] github.com/martinmoene/expected-lite: https://github.com/martinmoene/expected-lite
[12] std::size: http://en.cppreference.com/w/cpp/iterator/size
[13] std::data: http://en.cppreference.com/w/cpp/iterator/data
[14] lexically_normal: http://en.cppreference.com/w/cpp/filesystem/path/lexically_normal
[15] exists: http://en.cppreference.com/w/cpp/filesystem/exists
[16] 2038: остался всего 21 год: https://habrahabr.ru/company/pvs-studio/blog/328054/
[17] u8string: http://en.cppreference.com/w/cpp/filesystem/path/string
[18] u8path: http://en.cppreference.com/w/cpp/filesystem/path/u8path
[19] std::clamp: http://en.cppreference.com/w/cpp/algorithm/clamp
[20] to_chars: http://en.cppreference.com/w/cpp/utility/to_chars
[21] from_chars: http://en.cppreference.com/w/cpp/utility/from_chars
[22] Источник: https://habrahabr.ru/post/343622/
Нажмите здесь для печати.