- PVSM.RU - https://www.pvsm.ru -
В разработке существует множество ситуаций, когда вам надо выразить что-то с помощью "optional
" — объекта, который может содержать какое-либо значение, а может и не содержать. Вы можете реализовать опциональный тип с помощью нескольких вариантов, но с помощью C++17 вы сможете реализовать это с помощью наиболее удобного варианта: std::optional.
Сегодня я приготовил для вас одну задачу по рефакторингу, на который вы сможете научиться тому, как применять новую возможность C++17.
Давайте быстро погрузимся в код.
Представим, что есть функция, которая принимает объект ObjSelection
, представляющий из себя, например, текущую позицию указателя мыши. Функция сканирует выделение и находит количество анимированных объектов, есть ли там гражданские юниты и есть ли там военные юниты.
Существующий код выглядит так:
class ObjSelection
{
public:
bool IsValid() const { return true; }
// more code...
};
bool CheckSelectionVer1(const ObjSelection &objList,
bool *pOutAnyCivilUnits,
bool *pOutAnyCombatUnits,
int *pOutNumAnimating);
Как вы можете видеть выше, функция содержит в основном выходные параметры (в виде сырых указателей) и возвращает true/false
для индикации успеха своег выполнения (например, выделение может быть некорректным).
Я пропущу реализацию этой функции, но ниже вы можете увидеть код, который вызывает эту функцию:
ObjSelection sel;
bool anyCivilUnits { false };
bool anyCombatUnits {false};
int numAnimating { 0 };
if (CheckSelectionVer1(sel, &anyCivilUnits, &anyCombatUnits, &numAnimating))
{
// ...
}
Почему эта функция не идеальна?
На это есть несколько причин:
Что-нибудь ещё?
Как вы будете рефакторить это?
Руководствуясь Core Guidelines и новыми возможностями C++17, я планирую разделить рефакторинг на следующие шаги:
std::tuple
, который будет возвращаемым значением.std::tuple
в отдельную структуру и уменьшение std::tuple
до std::pair
.std::optional
чтобы подчеркнуть возможные ошибки.Эта статья является частью моей серии про библиотечные утилиты C++17. Вот список других тем, про которые я рассказываю:
Использование std::optional
(англ. язык). [2]Обработка ошибок при использовании std::optional
(англ. язык). [3]std::variant
.std::any
.std::optional
, std::variant
и std::any
.std::string_view
.std::filesystem
.Ресурсы по C++17 STL:
OK, теперь давайте что-нибудь порефакторим.
std::tuple
Первый шаг — это конвертировать выходные параметры в std::tuple
и вернуть его из функции.
В соответствии с F.21: Для возврата нескольких выходных значений предпочтительно использовать кортежи или структуры (англ. язык) [7]
Возвращаемое значение документируется само как значение "только для возврата". Учтите, что функция в C++ может иметь несколько возвращаемых значений с помощью соглашения об использовании кортежей (в т. ч. и пар (
std::pair
), с дополнительным использованием (возможно)std::tie
на вызывающей стороне.
После изменения наш код должен выглядеть вот так:
std::tuple<bool, bool, bool, int>
CheckSelectionVer2(const ObjSelection &objList)
{
if (!objList.IsValid())
return {false, false, false, 0};
// local variables:
int numCivilUnits = 0;
int numCombat = 0;
int numAnimating = 0;
// scan...
return {true, numCivilUnits > 0, numCombat > 0, numAnimating };
}
Немного лучше, не правда ли?
Более того, вы можете использовать структурированные привязки (англ. язык: Structured Bindings, прим. пер.: на русский язык пока нет устоявшегося названия) [8] для того, чтобы обернуть кортеж на вызывающей стороне:
auto [ok, anyCivil, anyCombat, numAnim] = CheckSelectionVer2(sel);
if (ok)
{
// ...
}
К сожалению, мне кажется, что это не самый лучший вариант. Я думаю, что легко забыть порядок выходных переменных в кортеже. На эту тему есть статья на SimplifyC++: Попахивающие std::pair
и std::tuple
(англ. язык) [9].
Более того, до сих пор остаётся проблема расширения функции в будущем. Поэтому, когда вы захотите добавить ещё одно выходное значение, вам надо будет расширять кортеж и на вызывающей стороне.
Поэтому я предлагаю следующий шаг: структура (это же предлагается в Core Guidelines).
Выходные результаты представляют собой связанные данные. Поэтому, похоже, хорошая идея обернуть их в структуру с именем SelectionData
:
struct SelectionData
{
bool anyCivilUnits { false };
bool anyCombatUnits { false };
int numAnimating { 0 };
};
После этого мы можем переписать нашу функцию следующим образом:
std::pair<bool, SelectionData> CheckSelectionVer3(const ObjSelection &objList)
{
SelectionData out;
if (!objList.IsValid())
return {false, out};
// scan...
return {true, out};
}
И на вызывающей стороне:
if (auto [ok, selData] = CheckSelectionVer3(sel); ok)
{
// ...
}
Я использовал std::pair
, поэтому мы всё ещё сохраняем флаг успешной отработки функции, он не становится частью новой структуры.
Основное преимущество в том, что мы получили здесь логическую структуру и расширяемость. Если вы хотите добавить новый параметр, просто расширьте структуру.
Но std::pair<bool, MyType>
ведь очень похожа на std::optional
, не так ли?
std::optional
Ниже описание типа std::optional
с CppReference [10]:
Шаблонный класс
std::optional
управляет опциональным значением, т. е. значением, которое может быть представлено, а может и не быть.
Обычным примером использования опционального типа данных является возвращаемое значение функции, которая может вернуть ошибочный результат в процессе выполнения. В отличии от других подходов, таких какstd::pair<T, bool>
, опциональный тип данных хорошо управляется с тяжёлыми для конструирования объектами и является более читабельным, поскольку явно выражает намерения разработчика.
Это, кажется, идеальный выбор для нашего кода. Мы можем убрать ok
из нашего кода и полагаться на семантику опционального типа.
Для справки, std::optional
был добавлен в C++17, но до C++17 вы могли бы использовать boost::optional
, так как они практически идентичны.
Новая версия нашего кода выглядит так:
std::optional<SelectionData> CheckSelection(const ObjSelection &objList)
{
if (!objList.IsValid())
return { };
SelectionData out;
// scan...
return {out};
}
и на вызывающей стороне:
if (auto ret = CheckSelection(sel); ret.has_value())
{
// access via *ret or even ret->
// ret->numAnimating
}
У версии с опциональным типом данных следующие преимущества:
T
. Мне кажется, что версия с использованием опционального типа является лучшей в рассмотренном примере.
Вы можете поиграть с кодом по этой ссылке [11].
В этой статье вы увидели как можно отрефакторить много плохо пахнущего кода с выходными параметрами с использованием опционального типа. Обёртка над данными в виде опционального типа ясно даёт понять, что вычисляемое значение может и не существовать. Так же я показал как обернуть несколько параметров функции в отдельную структуру. Вы можете легко расширять свой код с использованием отдельных типов данных, одновременно сохраняя логическую структуру кода.
С другой стороны, эта новыя реализация опускает важный аспект кода: обработка ошибок. На текущий момент вы не сможете узнать, по какой причине функция не смогла вычислить значение. В предыдущем примере, при реализации с std::pair
, мы могли бы возвращать какой-либо код ошибки для указания причины.
Вот что я нашёл в документации boost (англ. язык) [12]:
Опциональный тип данных
std::optional<T>
рекомендуется использовать в тех случаях, когда есть всего лишь одна причина, почему мы не смогли получить объект типаT
и где отсутствие значенияT
так же нормально, как и его наличие.
Другими словами, версия std::optional
выглядит отлично только в том случае, если мы принимаем ситуацию "некорректного выделения" за обычную рабочую ситуацию в приложении… это хорошая тема для следующей статьи :) Мне интересно, что вы думаете о тех местах, где было бы здорово использовать std::optional
.
Как бы вы отрефакторили первую версию кода?
Вы бы возвращали кортежи или создавали бы из них структуры?
Смотрите следующую статью: Использование std::optional
[13].
Ниже вы можете увидеть некоторые статьи, которые помогли мне с этим постом:
Беглый C++: Упрощение интерфейсов с использованием std::optional<T>
[16]Автор: NeonMercury
Источник [17]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/281177
Ссылки в тексте:
[1] F.20: Для возвращаемых значений предпочитайте возвращаемые значения из функции, а не выходные параметры: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#f20-for-out-output-values-prefer-return-values-to-output-parameters
[2] Использование std::optional
(англ. язык).: https://habr.com/post/372103/
[3] Обработка ошибок при использовании std::optional
(англ. язык).: https://www.bfilipek.com/2018/05/errors-and-optional.html
[4] Полное руководство по C++17: https://leanpub.com/cpp17
[5] Основы C++, включая C++17: https://pluralsight.pxf.io/c/1192940/424552/7490?u=https%3A%2F%2Fwww.pluralsight.com%2Fcourses%2Fcplusplus-fundamentals-c17
[6] Книга рецептов C++17 STL: http://amzn.to/2v6KkmV
[7] F.21: Для возврата нескольких выходных значений предпочтительно использовать кортежи или структуры (англ. язык): https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#f21-to-return-multiple-out-values-prefer-returning-a-tuple-or-struct
[8] структурированные привязки (англ. язык: Structured Bindings, прим. пер.: на русский язык пока нет устоявшегося названия): https://www.bfilipek.com/2017/07/cpp17-details-simplifications.html#structured-binding-declarations
[9] Попахивающие std::pair
и std::tuple
(англ. язык): https://arne-mertz.de/2017/03/smelly-pair-tuple/
[10] std::optional
с CppReference: http://en.cppreference.com/w/cpp/utility/optional
[11] ссылке: https://tech.io/playground-widget/85cf8cbfb026d494ea01678ab4b862ba0385/c-tests-2/275561/std%3A%3Aoptional%20refactor%20
[12] документации boost (англ. язык): https://www.boost.org/doc/libs/1_63_0/libs/optional/doc/html/boost_optional/tutorial/when_to_use_optional.html
[13] Использование std::optional
: https://www.bfilipek.com/2018/05/using-optional.html
[14] Эффективные опциональные значения (англ. язык): https://akrzemi1.wordpress.com/2015/07/15/efficient-optional-values/
[15] Ссылочные спецификаторы (англ. язык): https://akrzemi1.wordpress.com/2014/06/02/ref-qualifiers/
[16] Беглый C++: Упрощение интерфейсов с использованием std::optional<T>
: https://www.fluentcpp.com/2016/11/24/clearer-interfaces-with-optionalt/
[17] Источник: https://habr.com/post/369811/?utm_campaign=369811
Нажмите здесь для печати.