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

Программисты читают код намного чаще, чем пишут его, поэтому важно писать понятный, последовательный, однозначный код. Автор книги С++17 in detail написал о способах избегать путаницы. Делимся его материалом к старту курса по разработке на С++ [1].
Логические параметры в функциях могут вводить в заблуждение и затруднять читаемость кода, если имя функции неинформативно:
DoImportantStuff(true, false, true, false);
Неясно, что означают эти параметры. Что значит первый true или последний false? Можно ли в таких случаях сделать код лучше? Давайте посмотрим на возможный рефакторинг.
Эта статья вдохновлена похожим текстом, который появился в блоге Анджея Кржеменски: Toggles in functions [2]. Как пишет Анджей [3], весь смысл в том, чтобы улучшить код таких функций:
RenderGlyphs(glyphs, true, false, true, false);
Что если изменить порядок параметров? Тогда компилятор не слишком поможет. Подумаем о том, как сделать код лучше: сделаем его более безопасным и читаемым. Добавим комментарии:
RenderGlyphs(glyphs,
/*useChache*/true,
/*deferred*/false,
/*optimize*/true,
/*finalRender*/false);
И хотя код выше читается немного лучше, по-прежнему можно сделать его безопаснее. Но можно ли добиться большего?
Вот несколько идей:
Напишем такое объявление:
enum class UseCacheFlag { False, True };
enum class DeferredFlag { False, True };
enum class OptimizeFlag { False, True };
enum class FinalRenderFlag { False, True };
// и вызов, например:
RenderGlyphs(glyphs,
UseCacheFlag::True,
DeferredFlag::False,
OptimizeFlag::True,
FinalRenderFlag::False);
И нужно изменить реализацию:
if (useCache) { }
else { }
if (deferred) { }
else {}
Код сравнения:
if (useCache == UseCacheFlag::True) { }
else { }
if (deferred == DeferredFlag::True) { }
else {}
Как видите, теперь нужно проверить значения перечислений, а не просто значения bool. Использовать перечисления — это хороший подход, но он имеет свои недостатки:
Требует много дополнительных имён.
Может быть, возможно многократно применять некоторые типы. Нужно ли иметь определённые общие флаги?
Значения не конвертируются в логические напрямую, поэтому сравнивать Flag::True нужно явно, внутри тела функции.
Требуемое явное сравнение — причина появления маленькой библиотеки [4] Анджея, которая создаёт конвертируемые в bool переключатели. Я был разочарован отсутствием в языке непосредственной поддержки сильной типизации для перечислений. Но позже стал думать иначе.
Явное сравнение нетрудно написать, так что, возможно, включение сильной типизации в язык — перегиб? Явное преобразование типов может даже вызвать некоторые проблемы. Тем не менее я не совсем доволен необходимостью писать так много крошечных перечислений…
Как потенциальное развитие перечислений могут использоваться битовые флаги. К сожалению, у нас нет их дружественной и типобезопасной поддержки в языке, поэтому нужно добавить немного бойлерплейта.
Вот упрощённый подход:
#include <type_traits>
struct Glyphs { };
enum class RenderGlyphsFlags
{
useCache = 1,
deferred = 2,
optimize = 4,
finalRender = 8,
};
// упрощение...
RenderGlyphsFlags operator | (RenderGlyphsFlags a, RenderGlyphsFlags b) {
using T = std::underlying_type_t <RenderGlyphsFlags>;
return static_cast<RenderGlyphsFlags>(static_cast<T>(a) | static_cast<T>(b));
// todo: пропущенные проверки, находится ли значение в нужном диапазоне...
}
constexpr bool IsSet(RenderGlyphsFlags val, RenderGlyphsFlags check) {
using T = std::underlying_type_t <RenderGlyphsFlags>;
return static_cast<T>(val) & static_cast<T>(check);
// todo: пропущенные дополнительные проверки...
}
void RenderGlyphs(Glyphs &glyphs, RenderGlyphsFlags flags)
{
if (IsSet(flags, RenderGlyphsFlags::useCache)) { }
else { }
if (IsSet(flags, RenderGlyphsFlags::deferred)) { }
else { }
// ...
}
int main() {
Glyphs glyphs;
RenderGlyphs(glyphs, RenderGlyphsFlags::useCache | RenderGlyphsFlags::optimize);
}
Экспериментировать с кодом можно в @Compiler Explorer [5].
Что вы думаете об этом подходе? С некоторым дополнительным кодом и перегрузкой операторов в итоге можно получить типобезопасную, читабельную и красивую функцию. Добавив в мой код проверки, вы убедитесь, что в передаваемых значениях установлен нужный бит.
С версии С++23 можно воспользоваться std::to_underlying() из заголовочного файла <utility>. Эта функция уже реализована в GCC, Clang и MSVC: посмотрите мой пример в @Compiler Explorer [6].
Если у вас есть несколько параметров, четыре или пять, в зависимости от контекста, почему бы не обернуть их в отдельные структуры?
struct RenderGlyphsParam
{
bool useCache;
bool deferred;
bool optimize;
bool finalRender;
};
void RenderGlyphs(Glyphs &glyphs, const RenderGlyphsParam &renderParam);
// вызов:
RenderGlyphs(glyphs,
{/*useCache*/true,
/*deferred*/false,
/*optimize*/true,
/*finalRender*/false});
Это не очень помогло. Получился дополнительный код управления, а вызывающая сторона использует практически тот же код. Да, этот подход имеет следующие преимущества:
Он перемещает проблему. Применить сильную типизацию вы можете к отдельным членам структуры.
Если нужно больше параметров, можно расширить структуру.
Подход особенно полезен, когда много функций содержат одни и те же элементы структуры.
Переменную glyphs можно положить в RenderGlyphsParam, это только пример.
Благодаря обозначаемым инициализаторам, пришедшим в С++20, при конструировании структуры можно использовать именованные параметры. В основном при именовании передаваемых в функцию параметров вы можете воспользоваться подходом, как в именах аргументов С99:
struct RenderGlyphsParam
{
bool useCache;
bool deferred;
bool optimize;
bool finalRender;
};
void RenderGlyphs(Glyphs &glyphs, const RenderGlyphsParam &renderParam);
// вызов:
RenderGlyphs(glyphs,
{.useCache = true,
.deferred = false,
.optimize = true,
.finalRender = false});
Посмотреть код в @Compiler Explorer [7].
Об этой новой функциональности можно прочитать в моём посте Designated Initializers in C++20 [8].
Мы можем попробовать исправить синтаксис и написать понятный метод. Но что, если использовать метод ещё проще? Предоставить больше функций и просто устранить параметр?
Нормально иметь один или два параметра-переключателя, но, если их больше, это может означать, что функция пытается сделать слишком много. В нашем простом примере попробуем следующее разделение:
RenderGlyphsDeferred(glyphs,
/*useCache*/true,
/*optimize*/true);
RenderGlyphsForFinalRender(glyphs,
/*useCache*/true,
/*optimize*/true;
Сделаем изменение во взаимоисключающих параметрах: deferred и final не выполняются одновременно. Если разделить код не получится, можно иметь внешнюю функцию RenderGlyphsInternal.
Эта функция всё так же принимала бы эти параметры-переключатели. Но, по крайней мере, такой внутренний код будет скрыт от открытого API. Если это возможно, позже перепишите внешнюю функцию.
Думаю, полезно посмотреть на объявление функции и пересмотреть её на предмет взаимоисключающих параметров. Может, функция делает слишком много? Если да, разбейте её на функции меньше.
Написав этот раздел, я обратил внимание на совет Мартина Фаулера в статье [9], где он также пробует избегать переключателей. Можно прочитать эту статью здесь [10] и ещё больше в книге [11] Clean Code: A Handbook of Agile Software Craftsmanship.
Крошечные перечисления — это часть более общей темы применения усиленной типизации. Похожие проблемы могут появиться, когда ваши параметры — это несколько целых чисел или строк. Подробности читайте здесь:
К счастью, у нас есть руководства по С++, куда мы можем обратиться за помощью. Вот одно из них: I.4: Make interfaces precisely and strongly typed [18], в нём рассказывается не только о логических параметрах, но и обо всех потенциально вводящих в заблуждение именах. Например:
draw_rect(100, 200, 100, 500); // what do the numbers specify?
draw_rect(p.x, p.y, 10, 20); // what units are 10 and 20 in?
Чтобы сделать код лучше, воспользуемся следующими подходами:
Передадим отдельную структуру, чтобы аргументы конвертировались в члены данных.
Рассмотрим использование флагов перечисления.
Передадим в какую-нибудь функцию std::chrono::milliseconds, а не int num_msec.
Более того, ниже предлагаемые инструментами анализа кода обязательные правила: посмотрите на функцию со множеством примитивных аргументов.
Если говорить об инструментах, один читатель предложил [19] проверку Clang-Tidy, которая заставляет [20] писать "комментарии именованных типов" рядом с аргументами. Эта функциональность называется bugprone-argument-comment.
Пример её работы:
void RenderGlyphs(Glyphs &glyphs,
bool useCache, bool deferred, bool optimize, bool finalRender, int bpp)
{
}
int main() {
Glyphs glyphs;
RenderGlyphs(glyphs,
/*useCha=*/true,
/*deferred=*/false,
/*optimize=*/true,
/*finalRender=*/false,
/*bpppp=*/8);
}
Вы получите такое сообщение:
<source>:13:14: warning: argument name 'useCha' in comment does not
match parameter name 'useCache' [bugprone-argument-comment]
/*useCha=*/true,
^
<source>:5:8: note: 'useCache' declared here
bool useCache, bool deferred, bool optimize, bool finalRender, int bpp)
^
Форма комментария должна быть такой: /*arg=*/. Посмотрите пример в @Compiler Explorer [21].
Недавно у меня была возможность применить некоторые идеи перечисления/усиленных типов в моём коде. Вот грубый набросок:
// функции:
bool CreateContainer(Container *pOutContainer, bool *pOutWasReused);
void Process(Container *pContainer, bool bWasReused);
// применение
bool bWasReused = false;
if (!CreateContainer(&myContainer, &bWasReused))
return false;
Process(&myContainer, bWasReused);
Коротко: создаётся и обрабатывается контейнер. Он может применяться повторно через пул, повторное использование объектов, внутреннюю логику и т. д. Думаю, это некрасиво. Используется флаг, затем он передаётся какой-то другой функции.
Более того, мы передаём указатели, и должна быть дополнительная валидация. Кроме того, выходные параметры в современном С++ обескураживают, так что это всё равно не лучшая идея. Можно ли сделать лучше? Да. С помощью перечислений:
enum class ContainerCreateInfo { Err, Created, Reused };
ContainerCreateInfo CreateContainer(Container *pOutContainer);
void Process(Container *pContainer, ContainerCreateInfo createInfo);
// применение
auto createInfo = CreateContainer(&myContainer)
if (createInfo == ContainerCreateInfo::Err);
return false;
Process(&myContainer, createInfo);
Здесь нет вывода через указатели. Есть сильный тип для параметра-переключателя. Если перечислению CreateInfo нужно передать больше информации, можно просто добавить элемент перечисления и обработать в подходящем месте; прототипы функций не должны меняться.
Конечно, в реализации можно сравнивать значения перечислений, а не просто bool, но это несложно и даже подробнее. Код по-прежнему несовершенный, поскольку имеется pOutContainer, что неидеально.
В моём реальном проекте изменить это было сложно, и хотелось повторно использовать существующие контейнеры. Но если ваш контейнер поддерживает семантики перемещения и вы можете полагаться на оптимизацию возвращаемого значения, то возможно вернуть его:
enum class ContainerCreateInfo { Err, Created, Reused };
std::pair<Container, ContainerCreateInfo> CreateContainer();
Наша функция становится фабрикой функций, даже возвращает дополнительную информацию о процессе создания. Использовать это можно так:
// применение
auto [myContainer, createInfo] = CreateContainer()
if (createInfo == ContainerCreateInfo::Err);
return false;
Process(&myContainer, createInfo);
Читая оригинальную статью [22]Анджея и эти дополнения от меня, я надеюсь, вы поняли идею о параметрах-переключателях. Они не совсем ошибочны, и, вероятно, невозможно избегать их полностью.
По-прежнему хорошо пересматривать ваш дизайн, если захочется добавить три или четыре параметра в ряд. Может быть, вы сможете сократить количество переключателей/флагов, получив более выразительный код.
Список для чтения:
Вы пробовали переписать параметры-переключатели?
Использовали ли вы сильную типизацию в коде?
Поделитесь отзывом в комментариях.
А мы поможем прокачать ваши навыки и освоить профессию, которая останется востребованной в любое время:
Выбрать другую востребованную профессию [27].

Data Science и Machine Learning
Python, веб-разработка
Мобильная разработка
Java и C#
От основ — в глубину
А также
Курс по DevOps [48]
Все курсы [49]
Автор:
honyaki
Источник [50]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/372692
Ссылки в тексте:
[1] разработке на С++: https://skillfactory.ru/c-plus-plus-razrabotchik?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=coding_cplus_050322&utm_term=lead
[2] Toggles in functions: https://akrzemi1.wordpress.com/2017/02/16/toggles-in-functions/
[3] Анджей: https://akrzemi1.wordpress.com/about/
[4] библиотеки: https://github.com/akrzemi1/explicit/blob/master/include/ak_toolkit/tagged_bool.hpp
[5] @Compiler Explorer: https://godbolt.org/z/T6zaa9csP
[6] @Compiler Explorer: https://godbolt.org/z/KeoGq5EjK
[7] @Compiler Explorer: https://godbolt.org/z/Yjcbocaza
[8] Designated Initializers in C++20: https://www.cppstories.com/2021/designated-init-cpp20/
[9] статье: https://martinfowler.com/bliki/FlagArgument.html
[10] здесь: http://www.informit.com/articles/article.aspx?p=1392524
[11] книге: http://amzn.to/2m3g2LS
[12] Strong Types in C++: A Concrete Example: https://www.cppstories.com/2021/strong-types-pesel/
[13] Simplify C++: Use Stronger Types!: https://arne-mertz.de/2016/11/stronger-types/
[14] Type safe handles in C++: http://www.ilikebigbits.com/blog/2014/5/6/type-safe-identifiers-in-c
[15] Strong types for strong interfaces: http://www.fluentcpp.com/2016/12/08/strong-types-for-strong-interfaces/
[16] Type safe - Zero overhead utilities for more type safety: http://foonathan.net/blog/2016/10/11/type-safe.html
[17] Serialization - BOOST_STATIC_WARNING: http://www.boost.org/doc/libs/1_61_0/libs/serialization/doc/strong_typedef.html
[18] I.4: Make interfaces precisely and strongly typed: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#i4-make-interfaces-precisely-and-strongly-typed
[19] предложил: https://www.reddit.com/r/cpp/comments/sxpsxt/ways_to_refactor_toggleboolean_parameters_in_c/hxtweel/?utm_source=share&utm_medium=web2x&context=3
[20] заставляет: https://clang.llvm.org/extra/clang-tidy/checks/bugprone-argument-comment.html
[21] @Compiler Explorer: https://godbolt.org/z/e1Y71z989
[22] статью : https://www.cppstories.com/2017/03/on-toggle-parameters/(https://akrzemi1.wordpress.com/2017/02/16/toggles-in-functions/).
[23] What is wrong with boolean parameters?: https://understandlegacycode.com/blog/what-is-wrong-with-boolean-parameters/
[24] c++11 - Using scoped enums for bit flags in C++: https://softwareengineering.stackexchange.com/questions/194412/using-scoped-enums-for-bit-flags-in-c
[25] Профессия C++ разработчик: https://skillfactory.ru/c-plus-plus-razrabotchik?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=coding_cplus_050322&utm_term=conc
[26] Профессия Fullstack-разработчик на Python: https://skillfactory.ru/python-fullstack-web-developer?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=coding_fpw_050322&utm_term=conc
[27] востребованную профессию: https://skillfactory.ru/catalogue?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=sf_allcourses_050322&utm_term=conc
[28] Профессия Data Scientist: https://skillfactory.ru/data-scientist-pro?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=data-science_dspr_050322&utm_term=cat
[29] Профессия Data Analyst: https://skillfactory.ru/data-analyst-pro?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=analytics_dapr_050322&utm_term=cat
[30] Курс «Математика для Data Science»: https://skillfactory.ru/matematika-dlya-data-science#syllabus?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=data-science_mat_050322&utm_term=cat
[31] Курс «Математика и Machine Learning для Data Science»: https://skillfactory.ru/matematika-i-machine-learning-dlya-data-science?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=data-science_matml_050322&utm_term=cat
[32] Курс по Data Engineering: https://skillfactory.ru/data-engineer?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=data-science_dea_050322&utm_term=cat
[33] Курс «Machine Learning и Deep Learning»: https://skillfactory.ru/machine-learning-i-deep-learning?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=data-science_mldl_050322&utm_term=cat
[34] Курс по Machine Learning: https://skillfactory.ru/machine-learning?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=data-science_ml_050322&utm_term=cat
[35] Профессия Fullstack-разработчик на Python: https://skillfactory.ru/python-fullstack-web-developer?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=coding_fpw_050322&utm_term=cat
[36] Курс «Python для веб-разработки»: https://skillfactory.ru/python-for-web-developers?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=coding_pws_050322&utm_term=cat
[37] Профессия Frontend-разработчик: https://skillfactory.ru/frontend-razrabotchik?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=coding_fr_050322&utm_term=cat
[38] Профессия Веб-разработчик: https://skillfactory.ru/webdev?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=coding_webdev_050322&utm_term=cat
[39] Профессия iOS-разработчик: https://skillfactory.ru/ios-razrabotchik-s-nulya?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=coding_ios_050322&utm_term=cat
[40] Профессия Android-разработчик: https://skillfactory.ru/android-razrabotchik?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=coding_andr_050322&utm_term=cat
[41] Профессия Java-разработчик: https://skillfactory.ru/java-razrabotchik?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=coding_java_050322&utm_term=cat
[42] Профессия QA-инженер на JAVA: https://skillfactory.ru/java-qa-engineer-testirovshik-po?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=coding_qaja_050322&utm_term=cat
[43] Профессия C#-разработчик: https://skillfactory.ru/c-sharp-razrabotchik?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=coding_cdev_050322&utm_term=cat
[44] Профессия Разработчик игр на Unity: https://skillfactory.ru/game-razrabotchik-na-unity-i-c-sharp?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=coding_gamedev_050322&utm_term=cat
[45] Курс «Алгоритмы и структуры данных»: https://skillfactory.ru/algoritmy-i-struktury-dannyh?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=coding_algo_050322&utm_term=cat
[46] Профессия C++ разработчик: https://skillfactory.ru/c-plus-plus-razrabotchik?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=coding_cplus_050322&utm_term=cat
[47] Профессия Этичный хакер: https://skillfactory.ru/cyber-security-etichnij-haker?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=coding_hacker_050322&utm_term=cat
[48] Курс по DevOps: https://skillfactory.ru/devops-ingineer?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=coding_devops_050322&utm_term=cat
[49] Все курсы: https://skillfactory.ru/catalogue?utm_source=habr&utm_medium=habr&utm_campaign=article&utm_content=sf_allcourses_050322&utm_term=cat
[50] Источник: https://habr.com/ru/post/654253/?utm_source=habrahabr&utm_medium=rss&utm_campaign=654253
Нажмите здесь для печати.