Программисты читают код намного чаще, чем пишут его, поэтому важно писать понятный, последовательный, однозначный код. Автор книги С++17 in detail написал о способах избегать путаницы. Делимся его материалом к старту курса по разработке на С++.


Логические параметры в функциях могут вводить в заблуждение и затруднять читаемость кода, если имя функции неинформативно:

DoImportantStuff(true, false, true, false);

Неясно, что означают эти параметры. Что значит первый true или последний false? Можно ли в таких случаях сделать код лучше? Давайте посмотрим на возможный рефакторинг.

Введение

Эта статья вдохновлена похожим текстом, который появился в блоге Анджея Кржеменски: Toggles in functions. Как пишет Анджей, весь смысл в том, чтобы улучшить код таких функций:

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 нужно явно, внутри тела функции.

Требуемое явное сравнение — причина появления маленькой библиотеки Анджея, которая создаёт конвертируемые в 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.

Что вы думаете об этом подходе? С некоторым дополнительным кодом и перегрузкой операторов в итоге можно получить типобезопасную, читабельную и красивую функцию. Добавив в мой код проверки, вы убедитесь, что в передаваемых значениях установлен нужный бит.

С версии С++23 можно воспользоваться std::to_underlying() из заголовочного файла <utility>. Эта функция уже реализована в GCC, Clang и MSVC: посмотрите мой пример в @Compiler Explorer.

Параметры структуры

Если у вас есть несколько параметров, четыре или пять, в зависимости от контекста, почему бы не обернуть их в отдельные структуры?

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?

Благодаря обозначаемым инициализаторам, пришедшим в С++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.

Об этой новой функциональности можно прочитать в моём посте Designated Initializers in C++20.

Устранение логических параметров

Мы можем попробовать исправить синтаксис и написать понятный метод. Но что, если использовать метод ещё проще? Предоставить больше функций и просто устранить параметр?

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

RenderGlyphsDeferred(glyphs,
             /*useCache*/true, 
             /*optimize*/true);
RenderGlyphsForFinalRender(glyphs,
             /*useCache*/true, 
             /*optimize*/true;

Сделаем изменение во взаимоисключающих параметрах: deferred и final не выполняются одновременно. Если разделить код не получится, можно иметь внешнюю функцию RenderGlyphsInternal.

Эта функция всё так же принимала бы эти параметры-переключатели. Но, по крайней мере, такой внутренний код будет скрыт от открытого API. Если это возможно, позже перепишите внешнюю функцию.

Думаю, полезно посмотреть на объявление функции и пересмотреть её на предмет взаимоисключающих параметров. Может, функция делает слишком много? Если да, разбейте её на функции меньше.

Написав этот раздел, я обратил внимание на совет Мартина Фаулера в статье, где он также пробует избегать переключателей. Можно прочитать эту статью здесь и ещё больше в книге Clean Code: A Handbook of Agile Software Craftsmanship.

Усиленные типы

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

Руководства С++

К счастью, у нас есть руководства по С++, куда мы можем обратиться за помощью. Вот одно из них: I.4: Make interfaces precisely and strongly typed, в нём рассказывается не только о логических параметрах, но и обо всех потенциально вводящих в заблуждение именах. Например:

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.

Более того, ниже предлагаемые инструментами анализа кода обязательные правила: посмотрите на функцию со множеством примитивных аргументов.

Инструменты

Если говорить об инструментах, один читатель предложил проверку Clang-Tidy, которая заставляет писать "комментарии именованных типов" рядом с аргументами. Эта функциональность называется 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.

Конкретный пример 

Недавно у меня была возможность применить некоторые идеи перечисления/усиленных типов в моём коде. Вот грубый набросок:

// функции:
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);

Резюме 

Читая оригинальную статью Анджея и эти дополнения от меня, я надеюсь, вы поняли идею о параметрах-переключателях. Они не совсем ошибочны, и, вероятно, невозможно избегать их полностью.

По-прежнему хорошо пересматривать ваш дизайн, если захочется добавить три или четыре параметра в ряд. Может быть, вы сможете сократить количество переключателей/флагов, получив более выразительный код.

Список для чтения:

Немного вопросов:

  • Вы пробовали переписать параметры-переключатели?

  • Использовали ли вы сильную типизацию в коде?

Поделитесь отзывом в комментариях.

А мы поможем прокачать ваши навыки и освоить профессию, которая останется востребованной в любое время:

Выбрать другую востребованную профессию.

Краткий каталог курсов и профессий

Комментарии (25)


  1. staticmain
    05.03.2022 18:15
    +8

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

    Для 2, 3, 4 аргументов проблема по факту надумана. Если нужно передать больше — у вас что-то не то с архитектурой API. Нужно либо передать структуру, либо классы, которые будут содержать ряд нужных полей.

    Если вдруг (!) так получилось, что вам надо сделать вызов, который содержит 5 и более флагов, то самым наилучшим вариантом (если вы не можете разделить аргументы между несколькими классами и вызывать их конструкторы) будет сделать enum/enum class, содержащий перечисление возможных вариантов в человекочитаемом виде. Очень часто в жизни встречается вариация, когда программист о таком не задумывался, а затем в API можно увидеть следующее (локализовано в :ru: для понимания):
    Положение заслонки: истина

    P.S. Перевод статьи ужасный. Как будто засунули в google translate, а потом отредактировали окончания слов.


  1. gogibeet
    05.03.2022 18:18
    +11

    Есть некоторое ощущение , что если у вас 5 флагов в сигнатуре метода, то вы накосячили сильно раньше и исправлять надо явно не параметры.


  1. Sklott
    05.03.2022 19:04
    +1

    Для нас в похожем случае лучшим выходом было изменение имени функции, по типу:

    bool MatchCtrlAltShift(bool ctrl, bool alt, bool shift);

    и никаких проблем ни с чтением ни написанием.


    1. AVI-crak
      06.03.2022 13:53

      и никаких проблем ни с чтением ни написанием.

      Это будет работать в IDE, при наведении курсора на функцию будет всплывать подсказка. В том-же блокноте код будет выглядеть

      vall = MatchCtrlAltShift(true, true, false);

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

      Подобный экспериментальный плагин просмоторщика уже делали для С::B. И столкнулись с огромной кучей проблем. Чтоб плагин работал корректно - нужно переписать не только С::B, но и фреймворки из которых он создаётся. То-есть там изначально нет места для маневрирования. А в других IDE всё намного печальнее - там нужно начинать с чистого листа.

      Однако написать новый плагин, а может и IDE для него - гораздо дешевле чем делать очередную ветку развития в языках программирования, коих и так слишком много расплодилось.

      Всё написанное касается изменённого отображения параметров функций.


      1. Sulerad
        06.03.2022 15:04
        +4

        Удивительно, но такая фича уже существует, причем не вчера появилась. Называется inlay hints.

        Выглядит плюс-минус так:

        IntelliJ
        VS Code



      1. qw1
        06.03.2022 19:23

        Это будет работать в IDE, при наведении курсора на функцию будет всплывать подсказка
        Как я понял, идея в том, чтобы названия параметров включить в имя ф-ции, т.е. вместо
        void Render(double X, double Y, double Z, bool Optimize, bool Final);
        писать
        void RenderXYZOptmzFinal(double X, double Y, double Z, bool Optimize, bool Final);
        У идеи есть свои минусы, но право на жизнь тоже имеет.


  1. kovserg
    05.03.2022 19:39
    +10

    Зачем вам C++20? Чем так хреново?

    struct RenderGlyphsOptions {
      bool useChache,deferred,optimize,finalRender; int bpp;
      void setDefaults() {
        useChache=true;
        useChache=true, 
        deferred=false;
        optimize=true;
        finalRender=false;
        bpp=8;
      }
      RenderGlyphsOptions() { setDefaults(); }
    };
    void RenderGlyphs(Glyphs glyphs, RenderGlyphsOptions *options=0);
    ...
    RenderGlyphsOptions opts[1];
    opts->deferred=true;
    RenderGlyphs(glyphs,opts);
    ...
    

    Более того никто не запрещает писать так:
    bool useCache=true, optimize=true;
    RenderGlyphsDeferred(glyphs,useCache,optimize);
    

    И вообще вместо функции с кучей параметров, которые потом еще и будут дополняться, можно объявить вспомогательный класс
    struct RenderGlyphs {
      bool useChache,deferred,optimize,finalRender; int bpp;
      void setDefaults();
      RenderGlyphs() { setDefaults(); }
      void operator() (Glyphs glyphs) {}
    };
    ...
    RenderGlyphs render;
    render.optimize=false;
    render(glyphs);
    


    1. qw1
      06.03.2022 12:18
      +3

      Более того никто не запрещает писать так:
      bool useCache=true, optimize=true;
      RenderGlyphsDeferred(glyphs,useCache,optimize);

      Так в этом и проблема, никто не запрещает написать
      bool optimize=true, useCache=true;
      RenderGlyphsDeferred(glyphs,optimize,useCache);
      И ошибку увидеть практически невозможно в большой функции.


  1. WhiteWhiteWalker
    05.03.2022 20:50
    -4

    При использовании visual studio достаточно навести курсор на функцию чтобы высветилась её сигнатура. Может проблема в IDE или её отсутствии?


    1. IntActment
      06.03.2022 12:36
      +1

      Хочу заметить, часть удобств при работе действительно обеспечивается IDE в определенных деталях, которые мы упускаем из виду. Вот как раз мне вспомнился пример, применительный к статье: в визуал студии в сишарпе очень выгодно использовать перечисления вместо флагов благодаря фиче интеллисенса: как только ты переходишь к указанию аргумента перечисления, тип перечисления сразу подставляется в списке, и ты сразу же видишь все варианты. А вот в интеллисенсе c++ той же визуал студии почему-то так не работает: приходится помнить название конкретного энума (почему-то оно само интеллисенсом не подставляется), и тут уже использование перечислений в качестве флагов становится чуть менее удобно из-за необходимости переключать голову. Извиняюсь за сумбур, на словах такое сложно объяснить


      1. WhiteWhiteWalker
        06.03.2022 16:11

        Проверил в 13 студии, проект на C++, подсказка при написании вызова функции работает - вылезают имя enum'а и его значения.


  1. alxndrlsn
    05.03.2022 20:52
    -1

    Доброго времени суток!

    Простите за любопытстсво, а что мешает (начиная с С++17) использоват декомпозицию при объявлении (structural bindings)? Получается этакий "трюк", позволяющий уйти от FOR, снизить вероятность появления магических чисел на данном участке кода, а также позволяет не ломать голову над новыми названиями. Поправьте, если ошибаюсь.

    struct RenderGlyphsParam
    {
        bool useCache = true;
        bool deferred = false;
        bool optimize = true;
        bool finalRender = false;
    };
    
    // main
    RenderGlyphsParam structured;
    auto [useCache, deferred, optimize, finalRender] = structured;
    
    // вызов:
    RenderGlyphs(glyphs, useCache, deferred, optimize, finalRender);


  1. warlock13
    06.03.2022 13:39
    -2

    Вместо

    void RenderGlyphs(Glyphs &glyphs, const RenderGlyphsParam &renderParam);

    лучше так:

    void RenderGlyphs(Glyphs &glyphs, RenderGlyphsParam &&renderParam);


  1. avdosev
    06.03.2022 15:39
    -2

    Как вариант. Можно использовать enum, а не enum class. Тогда можно будет писать обычный код:


    enum MyBool : bool
    {
        True = true, False = false,
    };
    
    enum NotMyBool : bool
    {
        True = true, False = false,
    };
    
    void printBool(MyBool b) {
        if (b) {
            cout << "myTrue" << endl;
        } else {
            cout << "myFalse" << endl;
        }
    }

    Выглядит, конечно, не очень красиво, но лучше чем с enum class и при этом в printBool мы уже не сможем отправить NotMyBool — компилятор скажет об ошибке.


    1. qw1
      06.03.2022 19:29
      +1

      enum MyBool : bool
      {
          True = true, False = false,
      };
      
      enum NotMyBool : bool
      {
          True = true, False = false,
      };

      Не компилируется, ошибка
      1.cpp(8): error C2365: 'True': redefinition; previous definition was 'enumerator'
      1.cpp(3): note: see declaration of 'True'
      1.cpp(8): error C2365: 'False': redefinition; previous definition was 'enumerator'
      1.cpp(3): note: see declaration of 'False'


      1. avdosev
        06.03.2022 20:01

        Вы правы, чтобы не было redefinition необходимо их обернуть в namespace


        namespace MyBool {
        
        enum MyBool : bool
        {
            True = true, False = false,
        };
        
        }
        
        namespace NotMyBool {
        
        enum NotMyBool : bool
        {
            True = true, False = false,
        };
        
        }

        Забыл об этом указать. Поэтому это и не очень красивый вариант


        1. avdosev
          06.03.2022 22:34
          -1

          И интересный момент с помощью препроцессора эту портянку можно упростить используя этот макрос:


          #define NAMED_BOOL(name) \
          namespace _##name { \
          enum name : bool \
          { \
              True = true, False = false, \
          }; \
          } \
          using name = _##name::name ;

          Пример использования:


          #include <iostream>
          
          using namespace std;
          
          #define NAMED_BOOL(name) \
          namespace _##name { \
          enum name : bool \
          { \
              True = true, False = false, \
          }; \
          } \
          using name = _##name::name ;
          
          NAMED_BOOL(MyBool)
          NAMED_BOOL(NotMyBool)
          
          void printBool(MyBool a, NotMyBool b) {
              if (a && b) {
                  cout << "myTrue" << endl;
              } else {
                  cout << "myFalse" << endl;
              }
          }
          
          int main() {
              printBool(MyBool::True, NotMyBool::True); // compiled
              printBool(NotMyBool::True, MyBool::False); // not compiled
          }

          И это решение больше всего похоже на решение проблемы статьи.


          Мы не теряем в удобстве написания функции, тк работает он по прежнему как bool и получаем безопасность при вызове функций с булевыми переменными.


          А, и, кажется, должно работать даже для самых лохматых стандартов (с поправкой using на typedef) в том числе и в компайлтайм.


        1. mayorovp
          07.03.2022 09:36

          И чем это будет лучше enum class?


          1. avdosev
            07.03.2022 09:58

            Взгляните на код `printBool` чуть выше https://habr.com/ru/company/skillfactory/blog/654253/#comment_24141209 . В отличие от enum class обычный enum не требует статик кастов в bool.

            Работая с такими переменными вы пишите код внутри функции так будто это обычные булевы переменные, но получая все преимущества при вызове функции с булевыми значениями.

            Не придётся писать вот такой код или использовать tagged_bool (если по каким-то причинам он вам не подходит)

            if (mybool == MyBool::True)
            или
            if ((bool)mybool)


            1. mayorovp
              07.03.2022 10:21

              А что плохого в сравнении с MyBool::True? Особенно если вместо MyBool написать CacheOption, а вместо True — какой-нибудь Cached?


              1. avdosev
                07.03.2022 10:36
                +1

                Ничего плохого нет, но это уже будет совсем другой подход и вообще не булево значение.

                Лично мне интересно проработать именно булево значение. Понятно, что true/false не способны покрыть все возможные варианты развития архитектуры проекта, но я и не ставил такую цель, просто мне не понравилась необходимость сравнения с EnumClass::True.

                А сравнение с MyBool::True просто выглядит излишне сложным.

                Вы же не пишите в большинстве ситуаций:

                if (isBool == true)

                Любой человек на ревью вам скажет, что это бессмысленно и попросит написать так:

                if (isBool)


  1. frowrikrus
    06.03.2022 22:35
    +2

    Ох и наворотили) В c++ есть еще способ, сам пользуюсь, через структуру с функциями получается супер удобно. такой подход в tensorflow юзается))

    struct RenderGlyphsParam {
        bool useCache = true;
        bool deferred = false;
        bool optimize = true;
        bool finalRender = false;
    
        RenderGlyphsParam& SetUseCache(bool value) {
          useCache = value;
    			return *this;
        }
    		//..
    };
    RenderGlyphs(glyphs, RenderGlyphsParam().SetUseCache(true).SetDeferred(false) 
       /*и т.д.*/);


  1. Shadasviar
    08.03.2022 09:19
    -1

    А можно делать старые С перечисления,в которых названия значений сами себя документируют, типа USE_CACHE, NOT_USE_CACHE, и автоматическая конверсия к bool будет все делать сама, можно будет в коде и так и сяк использовать.


  1. g01dt00th
    08.03.2022 09:24

    для тех кто из плюсов уже ничего не помнит - до сих пор нет именованных параметров функций в языке?