Хабрахабр

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

Вы можете реализовать опциональный тип с помощью нескольких вариантов, но с помощью C++17 вы сможете реализовать это с помощью наиболее удобного варианта: std::optional. В разработке существует множество ситуаций, когда вам надо выразить что-то с помощью "optional" — объекта, который может содержать какое-либо значение, а может и не содержать.

Сегодня я приготовил для вас одну задачу по рефакторингу, на который вы сможете научиться тому, как применять новую возможность C++17.

Вступление

Давайте быстро погрузимся в код.

Функция сканирует выделение и находит количество анимированных объектов, есть ли там гражданские юниты и есть ли там военные юниты. Представим, что есть функция, которая принимает объект ObjSelection, представляющий из себя, например, текущую позицию указателя мыши.

Существующий код выглядит так:

class ObjSelection
{
public: bool IsValid() const // 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: Для возвращаемых значений предпочитайте возвращаемые значения из функции, а не выходные параметры)
  • Сырые указатели необходимо проверять на корректность.
  • Что насчёт расширения функции? Что если вам надо будет добавить ещё один выходной параметр?

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

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

Руководствуясь 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 и вернуть его из функции.

21: Для возврата нескольких выходных значений предпочтительно использовать кортежи или структуры (англ. В соответствии с F. язык)

Учтите, что функция в 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, прим. Более того, вы можете использовать структурированные привязки (англ. пер.: на русский язык пока нет устоявшегося названия) для того, чтобы обернуть кортеж на вызывающей стороне:

auto [ok, anyCivil, anyCombat, numAnim] = CheckSelectionVer2(sel);
if (ok)
{ // ...
}

Я думаю, что легко забыть порядок выходных переменных в кортеже. К сожалению, мне кажется, что это не самый лучший вариант. язык). На эту тему есть статья на SimplifyC++: Попахивающие std::pair и std::tuple (англ.

Поэтому, когда вы захотите добавить ещё одно выходное значение, вам надо будет расширять кортеж и на вызывающей стороне. Более того, до сих пор остаётся проблема расширения функции в будущем.

Поэтому я предлагаю следующий шаг: структура (это же предлагается в 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:

е. Шаблонный класс 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.
  • Нет надо беспокоиться насчёт лишних выделений памяти.

Мне кажется, что версия с использованием опционального типа является лучшей в рассмотренном примере.

Код

Вы можете поиграть с кодом по этой ссылке.

Итог

Обёртка над данными в виде опционального типа ясно даёт понять, что вычисляемое значение может и не существовать. В этой статье вы увидели как можно отрефакторить много плохо пахнущего кода с выходными параметрами с использованием опционального типа. Вы можете легко расширять свой код с использованием отдельных типов данных, одновременно сохраняя логическую структуру кода. Так же я показал как обернуть несколько параметров функции в отдельную структуру.

На текущий момент вы не сможете узнать, по какой причине функция не смогла вычислить значение. С другой стороны, эта новыя реализация опускает важный аспект кода: обработка ошибок. В предыдущем примере, при реализации с std::pair, мы могли бы возвращать какой-либо код ошибки для указания причины.

язык): Вот что я нашёл в документации boost (англ.

Опциональный тип данных std::optional<T> рекомендуется использовать в тех случаях, когда есть всего лишь одна причина, почему мы не смогли получить объект типа T и где отсутствие значения T так же нормально, как и его наличие.

Другими словами, версия std::optional выглядит отлично только в том случае, если мы принимаем ситуацию "некорректного выделения" за обычную рабочую ситуацию в приложении… это хорошая тема для следующей статьи 🙂 Мне интересно, что вы думаете о тех местах, где было бы здорово использовать std::optional.

Как бы вы отрефакторили первую версию кода?
Вы бы возвращали кортежи или создавали бы из них структуры?

Смотрите следующую статью: Использование std::optional.

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть