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

Рефакторинг с использованием C++17 std::optional

Рефакторинг с использованием C++17 std::optional - 1

В разработке существует множество ситуаций, когда вам надо выразить что-то с помощью "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 рекомендуют не использовать их. (F.20: Для возвращаемых значений предпочитайте возвращаемые значения из функции, а не выходные параметры [1])
  • Сырые указатели необходимо проверять на корректность.
  • Что насчёт расширения функции? Что если вам надо будет добавить ещё один выходной параметр?

Что-нибудь ещё?

Как вы будете рефакторить это?

Руководствуясь Core Guidelines и новыми возможностями C++17, я планирую разделить рефакторинг на следующие шаги:

  1. Рефакторинг выходных параметров в std::tuple, который будет возвращаемым значением.
  2. Рефакторинг std::tuple в отдельную структуру и уменьшение std::tuple до std::pair.
  3. Использование std::optional чтобы подчеркнуть возможные ошибки.

Серия

Эта статья является частью моей серии про библиотечные утилиты C++17. Вот список других тем, про которые я рассказываю:

Ресурсы по 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].

Ниже вы можете увидеть некоторые статьи, которые помогли мне с этим постом:

Автор: 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