Как стать автором
Обновить

Комментарии 22

Имеются в виду контекстные преобразования.

Есть один момент, который не все знают. Если объявить оператор преобразования как explicit:

    struct A {
        explicit operator bool() { return false; }
    };


то компилятор не сможет делать неявные преобразования:

    auto foo = [](bool) { return; };

    A a;
    foo(a); // не компилируется


А контекстные — сможет:

    while (a);
    do { if (a) assert(false); } while (a);
    assert(!a && (a || true));
    for(a ? 5 : 6; a;);
    // кроме того, explicit operator bool допускает использование объектов
    // класса в выражениях static_assert и noexcept

В Visual Studio раньше был варнинг C4800: 'type': forcing value to bool 'true' or 'false' (performance warning). Но в VS2017 эти идиоты его убрали, т.к. он, видишь ли, был слишком «шумный».
работа с байтами медленнее, чем с двойными словами (если речь про x86).
Нет, тут дело не в этом.
Проблема производительности в том, что если у нас есть переменная val целого типа, то компилятор в выражении
bool f = val;

может сгенерировать (внимание, ниже псевдокод для демонстрации поведения) нечто вроде такого:
bool f = val == 0 ? false : true;

Это предупреждение было призвано обозначить возможность потенциальной генерации такого кода в этом месте, на случай, если программист об этом не подумал.
Чтобы не быть голословным, приведу практический пример с дизассемблером:
godbolt.org/g/FQG8Jk
Как видим, в безобидном присваивании появилось условие. Данный warning об этом.

При этом, если посмотреть выданную выше ссылку, то можно увидеть в ответе на report такие слова
My understanding of the history of C4800 was that it was introduced a very long time ago (~1990?) to address concerns for developers migrating from C.
Это коссвенно указывает на тоже самое, о чем я говорил выше. Т.к. в С не было типа bool, то программисты на нем, при переходе на С++ могли быть неприятно удивлены, что одинаковый внешне код в С++ приводит к генерации «лишних» инструкций, чего в С не наблюдалось. Поэтому на каких-то этапах это предупреждение действительно было полезно.
Очевидно, автор использует два параметра типа bool как флаги (toggles)

Обычно, бывает так, что там где два подобных параметра, там со временем их становится 10. И как ты эти параметры не называй лучше от этого не станет.
Здесь нужно сосредоточить свои силы не на том как назвать эти параметры, а в принципе пересмотреть подобный интерфейс с отдачей наружу функции с 2-10 флажками (раз уж мы говорим о C++), тем более если это какая-та обработка. Например, декомпозировать, выделить отдельные сущности (возможно полиморфные), определить входные/выходные параметры, выстроить процесс обработки обходом четко определенных объектов (все зависит от конкретной задачи). Поддержать хоть какую-то гибкость и расширяемость.


Т.е. проблема здесь не во флажках, а в коде типа


...
if (withNewEngine)
    do_something_new();
else
    do_something_old();
...
Комментаторы в оригинальной статье тоже это заметили. После самой статьи я привожу перевод нескольких таких комментариев с ответами автора.
На самом деле часто бывает так:


// Many lines of code

if(flag) {
// A few lines
}
Проблема поставлена хорошо, да и решение предложено неплохое. Но ИМХО использование перечислений — шаг вперёд, а завязка их на булевы значения (и тем более каст к ним) — шаг назад. При определении перечислений нужно забыть, что переменные, из-за которых они возникают, были из множества {false, true}. Т.е. вместо
enum class WithValidation : bool { False, True };
enum class WithNewEngine  : bool { False, True };

я бы написал как-то так:
enum class ValidationMode : bool { NoValidation, Validation };
enum class EngineVersion : bool { Old, New };

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

Кстати, по опыту, часто функции (bool,…, bool) не обрабатывают весь спектр 2 в степени n индивидуально, она просто болеет «добавлением ещё флажка». Т.е., например, если у нас есть функция
void PaintControl( bool is_visible, bool is_enable );

То при первом аргументе false второй вообще не важен: какая разница, доступный контрол или нет, если мы его даже не будем отрисовывать по факту. Из-за этого напрашивается сделать что-то такое:
enum class ControlVisibility { Hidden, Disabled, Enabled };
void PaintControl( ControlVisibility visibility );
Ну, Вы уже зашли туда, где нужен объект с тремя состояниями в качестве параметра, а статья все-таки про флажки.

Автор пишет, что использовать в качестве флагов перечисления или tagged_bool — это вопрос личных предпочтений. Почитайте комментарий ARNAUD к оригинальной статье — я его тоже перевел.

К сожалению, часто такие функции из сторонних библиотек. И наряду с подходом добавления комментариев


process(/*withValidation=*/ false, /*withNewEngine=*/ true);

Существует подход именованных локальных переменных:


constexpr auto withValidation = false;
constexpr auto withNewEngine = false;
process(withValidation, withNewEngine);
Кажется мне, что читабельности это не добавляет. Булевы переменные должны читаться утвердительно. То есть
constexpr auto withValidation = true;
constexpr auto noValidation = false;
constexpr auto withNewEngine = true;
constexpr auto withOldEngine = false;
process(noValidation, withOldEngine);


Булевы переменные должны читаться утвердительно.

На мой взгляд, это зависит от гайдлайнов, принятых при кодировании. Мне, например, выражение с двойным отрицанием сильно режет глаз:


constexpr auto noValidation = false;

Для меня было бы привычней:


    {
        constexpr auto validation = false;
        process(validation);
    }

    {
        constexpr auto validation = true;
        process(validation);
    }

Кажется мне, что читабельности это не добавляет.

Часто это избавляет от перехода h-файлу (для того что бы увидеть прототип функции).

Главное, чтобы вызов функции читался однозначно. То есть или присвоение и вызов подряд (как у вас), или имя булевой переменной несёт в себе и значение (как у меня).
Но не определили переменную withValidation = false, а потом используем её кучу раз ниже по тексту.
В итоге, если отлаживаешь нижний вызов, то уже наивно думаешь, что process(withValidation) это всё-таки с валидацией.
В совсем крайнем случае можно флаг назвать нейтрально (Validation, Engine), как в вашем случае, чтобы не было ненужных ассоциаций

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


something.process();

Какие-то проблемы на ровном месте


enum { WithValidation=1, NewEngine=2 };
void process( int flags );
...
process( NewEngine | WithValidation );
НЛО прилетело и опубликовало эту надпись здесь
Вы создаёте лишние проблемы и сущности на ровном месте. Если не нравятся флаги можно писать так:
struct Process {
  typedef Process This;
  Process() {
    validation=false;
    new_engine=false;
  }
  int run() {
    //...
    return 0;
  }
  This& WithValidation(bool value=true) { validation=value; return *this; }
  This& UseNewEngine(bool value=true) { new_engine=value; return *this; }
protected:
  bool validation, new_engine;
};
...
Process process;
process
  .UseNewEngine()
  .WithValidation(false)
  .run();

Но с флагими работет. И работает хорошо. А как гласит народная мудрость: работает не трож.
--Я вообще не люблю флаги. Они противоречат принципу единственной ответственности.

этy проблему в какой-то мере решили в С# добавляя к любому типу состояние NULL.

bool? value[] = {true, false, null};
В таких случаях у нас обычно флаги конвертируются в set из enum-ов.
И сразу все становится понятно.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории