В разработке существует множество ситуаций, когда вам надо выразить что-то с помощью "optional" — объекта, который может содержать какое-либо значение, а может и не содержать. Вы можете реализовать опциональный тип с помощью нескольких вариантов, но с помощью C++17 вы сможете реализовать это с помощью наиболее удобного варианта: std::optional. Сегодня я приготовил для вас одну задачу по рефакторингу, на который вы сможете научиться тому, как применять новую возможность C++17. Давайте быстро погрузимся в код. Представим, что есть функция, которая принимает объект ObjSelection, представляющий из себя, например, текущую позицию указателя мыши. Функция сканирует выделение и находит количество анимированных объектов, есть ли там гражданские юниты и есть ли там военные юниты. Существующий код выглядит так: Как вы можете видеть выше, функция содержит в основном выходные параметры (в виде сырых указателей) и возвращает true/false для индикации успеха своег выполнения (например, выделение может быть некорректным). Я пропущу реализацию этой функции, но ниже вы можете увидеть код, который вызывает эту функцию: Почему эта функция не идеальна? На это есть несколько причин: Что-нибудь ещё? Как вы будете рефакторить это? Руководствуясь Core Guidelines и новыми возможностями C++17, я планирую разделить рефакторинг на следующие шаги: Эта статья является частью моей серии про библиотечные утилиты C++17. Вот список других тем, про которые я рассказываю: Ресурсы по C++17 STL: OK, теперь давайте что-нибудь порефакторим. Первый шаг — это конвертировать выходные параметры в std::tuple и вернуть его из функции. В соответствии с F.21: Для возврата нескольких выходных значений предпочтительно использовать кортежи или структуры (англ. язык) После изменения наш код должен выглядеть вот так: Немного лучше, не правда ли? Более того, вы можете использовать структурированные привязки (англ. язык: Structured Bindings, прим. пер.: на русский язык пока нет устоявшегося названия) для того, чтобы обернуть кортеж на вызывающей стороне: К сожалению, мне кажется, что это не самый лучший вариант. Я думаю, что легко забыть порядок выходных переменных в кортеже. На эту тему есть статья на SimplifyC++: Попахивающие std::pair и std::tuple (англ. язык). Более того, до сих пор остаётся проблема расширения функции в будущем. Поэтому, когда вы захотите добавить ещё одно выходное значение, вам надо будет расширять кортеж и на вызывающей стороне. Поэтому я предлагаю следующий шаг: структура (это же предлагается в Core Guidelines). Выходные результаты представляют собой связанные данные. Поэтому, похоже, хорошая идея обернуть их в структуру с именем SelectionData: После этого мы можем переписать нашу функцию следующим образом: И на вызывающей стороне: Я использовал std::pair, поэтому мы всё ещё сохраняем флаг успешной отработки функции, он не становится частью новой структуры. Основное преимущество в том, что мы получили здесь логическую структуру и расширяемость. Если вы хотите добавить новый параметр, просто расширьте структуру. Но std::pair<bool, MyType> ведь очень похожа на std::optional, не так ли? Ниже описание типа std::optional с CppReference: Это, кажется, идеальный выбор для нашего кода. Мы можем убрать ok из нашего кода и полагаться на семантику опционального типа. Для справки, std::optional был добавлен в C++17, но до C++17 вы могли бы использовать boost::optional, так как они практически идентичны. Новая версия нашего кода выглядит так: и на вызывающей стороне: У версии с опциональным типом данных следующие преимущества: Мне кажется, что версия с использованием опционального типа является лучшей в рассмотренном примере. Вы можете поиграть с кодом по этой ссылке. В этой статье вы увидели как можно отрефакторить много плохо пахнущего кода с выходными параметрами с использованием опционального типа. Обёртка над данными в виде опционального типа ясно даёт понять, что вычисляемое значение может и не существовать. Так же я показал как обернуть несколько параметров функции в отдельную структуру. Вы можете легко расширять свой код с использованием отдельных типов данных, одновременно сохраняя логическую структуру кода. С другой стороны, эта новыя реализация опускает важный аспект кода: обработка ошибок. На текущий момент вы не сможете узнать, по какой причине функция не смогла вычислить значение. В предыдущем примере, при реализации с std::pair, мы могли бы возвращать какой-либо код ошибки для указания причины. Вот что я нашёл в документации boost (англ. язык): Другими словами, версия std::optional выглядит отлично только в том случае, если мы принимаем ситуацию "некорректного выделения" за обычную рабочую ситуацию в приложении… это хорошая тема для следующей статьи :) Мне интересно, что вы думаете о тех местах, где было бы здорово использовать std::optional. Как бы вы отрефакторили первую версию кода?
Вы бы возвращали кортежи или создавали бы из них структуры? Смотрите следующую статью: Использование std::optional. Ниже вы можете увидеть некоторые статьи, которые помогли мне с этим постом: Expert Software Engineer