Вы когда-нибудь сталкивались с таким кодом?
Эта функция, судя по названию, что-то обрабатывает (process). Но что означают параметры? Какой параметр здесь true, а какой false? По вызывающему коду об этом нельзя судить.
Нам придется заглянуть в объявление функции, которое дает подсказку:
Очевидно, автор использует два параметра типа bool как флаги (toggles). Реализация функции может быть похожа на это:
Назначение флагов очевидно, поскольку каждый из них имеет осмысленное название. Проблема возникает в вызывающем коде. И дело не только в том, что мы не можем сразу понять, какие флаги используются. Даже зная это, мы легко можем перепутать их порядок. На самом деле, мой первый пример должен был выглядеть так:
Но я перепутал порядок аргументов.
Столкнувшись с этим багом, программист, вероятно, добавит комментарии к вызову функции, чтобы явно показать свои намерения:
И это слишком похоже на именованные параметры функции — возможность, отсутствующую в C++. Если б она была, то могла бы выглядеть как-нибудь так:
Но если бы даже в C++ такое было, вряд ли это было бы совместимо с прямой передачей (perfect forwarding):
С этим может быть связан еще более коварный баг, который гораздо труднее отследить. Представьте, что функция process — это виртуальный метод класса. И в каком-то другом классе мы его переопределяем, при этом располагая флаги в неправильном порядке:
Компилятор не заметит проблемы, поскольку параметры различаются только по именам, а их типы одинаковы (оба bool).
Баги, возникающие по причине использования логических параметров в интерфейсе, на этом не заканчиваются. Из-за того, что почти все встроенные типы преобразовываются в bool, следующий пример компилируется без ошибок, но делает не то, что ожидается:
Более распространенная проблема — с использованием bool в конструкторах. Пускай есть класс с двумя конструкторами:
В какой-то момент Вы решаете удалить второй конструктор, и может быть надеетесь, что компилятор укажет Вам на все места, требующие исправления. Но этого не происходит. Из-за неявных преобразований в bool, первый конструктор будет использован везде, где раньше использовался второй.
Однако есть причина, почему люди обычно используют bool для представления флагов. Это единственный встроенный тип, доступный «из коробки» и предназначенный для представления только двух возможных значений.
Чтобы решить указанные проблемы, мы должны располагать типом, отличным от bool, который удовлетворял бы следующим требованиям:
— для каждого флага создается уникальный тип,
— неявные преобразования запрещаются.
C++11 вводит понятие классов перечислений, которые удовлетворяют обоим требованиям. Также мы можем использовать тип bool как базовый тип перечисления; таким образом, мы гарантируем, что перечисление содержит только два возможных значения и имеет размер одного bool. Вначале определяем классы флагов:
Теперь мы можем объявить нашу функцию:
Есть некоторая избыточность в этом объявлении, но зато порядок использования функции теперь такой, какой нужен:
И если я поставлю флаги в неправильном порядке, то получу ошибку компиляции из-за несоответствия типов:
Каждый флаг имеет уникальный тип, который исправно работает при прямой передаче (perfect forwarding), и Вы никак не сможете расположить параметры в неправильном порядке в объявлениях функций и переопределениях виртуальных методов.
Но использование перечислений в качестве флагов имеет свою цену. Флаги в некоторой мере похожи на значения типа bool, но классы перечислений не имитируют эту схожесть. Неявные преобразования в bool и обратно не работают (и это хорошо), но явные преобразования тоже не работают, и это проблема. Если мы взглянем еще раз на тело функции process, то поймем, что оно не компилируется:
Мне приходится использовать явное преобразование:
И если мне понадобится логическое выражение с двумя флагами, то оно будет выглядеть еще неадекватнее:
Кроме того, для экземпляра класса перечисления Вы не можете сделать прямую инициализацию из bool:
Опять придется делать явное преобразование:
Это можно считать дополнительной гарантией безопасности, но здесь слишком много явных преобразований. В классах перечислений больше «explicit», чем в конструкторах и операторах преобразования, объявленных как «explicit».
Из-за проблем, возникающих при использовании bool и классов перечислений, мне пришлось сделать свой собственный инструмент, который называется tagged_bool. Вы можете найти его реализацию здесь. Она совсем небольшая. С ее помощью, классы флагов объявляются вот так:
Вам придется сделать предварительное объявление класса-тэга, такого как «WithValidation_tag». Определение для него писать не нужно. Он используется для создания уникальной специализации шаблона класса tagged_bool. Эта специализация может быть явно преобразована в bool и обратно, а также в другие специализации шаблона tagged_bool, поскольку, как обычно бывает на практике, какой-нибудь bool, передающийся на нижние уровни приложения, становится впоследствии другим флагом с другим именем. Использовать созданные таким образом флаги можно вот так:
Вот и все. tagged_bool — это часть библиотеки Explicit library, которая содержит несколько инструментов, позволяющих более явно выразить Ваши намерения при проектировании интерфейсов.
У Анджея ранее была другая статья про тэги — «Intuitive interface — Part I» от 5 июля 2013 г. (Part 2 так и не появилась на свет, не ищите). Если вкратце, то там поднималась такая проблема:
Когда поведение конструктора зависит от формы скобочек, это само по себе опасно. Кроме того, это делает непонятным вызывающий код:
Что такое 5 и 6? Это будет 5 шестерок или 6 пятерок? Если забыли — идите смотреть документацию.
И хотелось бы иметь еще один конструктор, создающий пустой вектор с заданной capacity: std::vector v(100). К сожалению, конструктор, принимающий один size_t, уже занят — он создает вектор с заданным size’ом, заполненный сконструированными по умолчанию объектами.
Анджей упоминает, что такой порядок вещей не дает в полной мере воспользоваться возможностями прямой передачи, но в комментариях ему разъяснили, что эта проблема решается без всяких тэгов.
Анджей пришел к выводу, что реализация вектора в библиотеке STL не вполне удачная. Было бы куда проще, если бы в его конструкторах использовались тэги:
Применительно к настоящей статье, это выглядело бы так:
Разница в том, что тэг и значение теперь объединены в один объект. В статье «Competing constructors» от 29 июля 2016 г. Анджей мимоходом написал, что ему не нравится идея такого объединения.
Теперь это уже не тэги, а полноценные объекты. Кому-нибудь может прийти в голову положить их в контейнер:
Поведение этого кода опять зависит от формы скобочек. Что за радость была вводить тэги, если мы снова вернулись к той же проблеме? По крайней мере, простые тэги вряд ли кому-нибудь захочется хранить в контейнере. Ведь они могут иметь только одно значение.
С bool, однако, эта угроза не так страшна. Конструкторы контейнеров STL не принимают bool, иначе как в составе initializer_list’ов. Видимо, поэтому Анджей и решился в этот раз объединить тэг и значение.
Напоследок приведу перевод нескольких комментариев к статье.
kszatan
February 17, 2017 at 11:36 am
Я бы в первую очередь подумал об избавлении от всех этих ��лагов и вывел бы код для подтверждений (validations) и нового/старого движка (new/old engine) в отдельные классы, чтобы передавать их как аргументы. Функция «process» и так уже делает слишком много.
Andrzej Krzemieński
February 17, 2017 at 12:03 pm
В простых случаях отказ от любых флагов действительно может оказаться лучшим выбором. Но когда решение установить флаг принимается несколькими уровнями выше в стеке вызовов, такой рефакторинг может оказаться неосуществимым или непрактичным.
===Конец треда===
micleowen
February 17, 2017 at 10:41 pm
«explicit» используется в конструкторах с одним параметром.
Andrzej Krzemieński
February 20, 2017 at 8:22 am
Есть основательная причина объявлять почти все конструкторы как «explicit», а не только конструкторы с 1 аргументом (особенно с выходом стандарта C++11). Иногда даже конструктор по умолчанию лучше объявить как «explicit». Кому интересно, советую обратиться вот к этой статье .
===Конец треда===
ARNAUD
February 18, 2017 at 6:39 pm
«Неявные преобразования в bool и обратно не работают (и это хорошо), но явные преобразования тоже не работают, и это проблема»
Не понимаю, что в этом плохого:
Сначала Вы используете общеизвестную возможность языка, и Ваш код прекрасно читается и понимается всеми специалистами по С++. Потом Вы переходите к использованию специального шаблона для автоматического преобразования в bool и обратно? Меня это не убеждает.
И еще. Представьте, что через какое-то время один из параметров перестанет быть bool и сможет принимать значения no_engine, engine_v1, engine_v2… Класс перечисления позволяет сделать такое расширение естественным путем, в отличие от Вашего tagged_bool.
Andrzej Krzemieński
February 20, 2017 at 8:36 am
Вы подняли два вопроса.
1. Выбор между
и
И, в случае использования пространств имен:
Для меня это компромисс между желаемым уровнем безопасности и удобством использования. Мой личный выбор — что-то безопаснее bool, но не такое многословное как классы перечислений. Видимо, Ваш компромисс лежит ближе к классам перечислений.
2. Возможность добавить третье состояние
Если Вы предвидите, что в будущем Вам может понадобиться третье состояние, то классы перечислений и правда могут быть предпочтительнее. А могут и не быть. Потому что, когда Вы добавляете третье состояние, все Ваши if’ы продолжают исправно компилироваться, хотя Вы, может быть, желаете их отредактировать, чтобы добавить проверку третьего состояния.
По моему опыту, эти ��лаги используются как временные решения, и их дальнейшее развитие не в том, чтобы добавить третье состояние, а в том, чтобы избавиться от двух имеющихся. Например, я улучшаю какую-то часть программы, но в течение пары месяцев хочу дать пользователям возможность переключиться обратно на старую реализацию, на случай, если я что-то недосмотрел, и улучшение только все испортит. Если после пары месяцев все пользователи остались довольны, я удаляю поддержку старой реализации и избавляюсь от флага.
===Конец треда===
mftdev00
March 13, 2017 at 1:05 pm
Я вообще не люблю флаги. Они противоречат принципу единственной ответственности. Делай что-то, если true, делай что-то другое, если false…
Andrzej Krzemieński
March 13, 2017 at 1:10 pm
Согласен. Везде, где это возможно, нужно обходиться без флагов.
===Конец треда===
SebB
March 21, 2017 at 6:09 pm
Можно ли вместо явного удаления конструкторов для каждого типа:
…просто удалить их для всех типов (кроме bool) разом?
Andrzej Krzemieński
March 22, 2017 at 7:32 am
Я просто не учел такую возможность, когда разрабатывал интерфейс. Может, и полезно было бы это добавить. Но теперь, когда Вы это предложили, я вижу один случай, где это имело бы отрицательный эффект: кто-то может использовать свой собственный (безопасный) логический тип с неявным преобразованием в bool. В этом случае, нам, может быть, нужно позволить этому типу работать с tagged_bool.
process(true, false);Эта функция, судя по названию, что-то обрабатывает (process). Но что означают параметры? Какой параметр здесь true, а какой false? По вызывающему коду об этом нельзя судить.
Нам придется заглянуть в объявление функции, которое дает подсказку:
void process(bool withValidation,
bool withNewEngine);Очевидно, автор использует два параметра типа bool как флаги (toggles). Реализация функции может быть похожа на это:
void process(bool withValidation,
bool withNewEngine)
{
if (withValidation) // используется 1-й флаг
validate(); // % подтвердить
do_something_toggle_independent_1
if (withNewEngine) // используется 2-й флаг
do_something_new();
else
do_something_old();
do_something_toggle_independent_2();
}Назначение флагов очевидно, поскольку каждый из них имеет осмысленное название. Проблема возникает в вызывающем коде. И дело не только в том, что мы не можем сразу понять, какие флаги используются. Даже зная это, мы легко можем перепутать их порядок. На самом деле, мой первый пример должен был выглядеть так:
process(false, true);Но я перепутал порядок аргументов.
Столкнувшись с этим багом, программист, вероятно, добавит комментарии к вызову функции, чтобы явно показать свои намерения:
process(/*withValidation=*/ false, /*withNewEngine=*/ true);И это слишком похоже на именованные параметры функции — возможность, отсутствующую в C++. Если б она была, то могла бы выглядеть как-нибудь так:
// В C++ такого нет:
process(withValidation: false, withNewEngine: true);Но если бы даже в C++ такое было, вряд ли это было бы совместимо с прямой передачей (perfect forwarding):
std::function<void(bool, bool)> callback = &process;
callback(???, ???); // какие имена использовать?С этим может быть связан еще более коварный баг, который гораздо труднее отследить. Представьте, что функция process — это виртуальный метод класса. И в каком-то другом классе мы его переопределяем, при этом располагая флаги в неправильном порядке:
struct Base
{
virtual void process(bool withValidation,
bool withNewEngine);
};
struct Derived : Base
{
void process(bool withNewEngine,
bool withValidation) override;
};Компилятор не заметит проблемы, поскольку параметры различаются только по именам, а их типы одинаковы (оба bool).
Баги, возникающие по причине использования логических параметров в интерфейсе, на этом не заканчиваются. Из-за того, что почти все встроенные типы преобразовываются в bool, следующий пример компилируется без ошибок, но делает не то, что ожидается:
std::vector<int> vec;
process(vec.data(), vec.size());Более распространенная проблема — с использованием bool в конструкторах. Пускай есть класс с двумя конструкторами:
class C
{
explicit C(bool withDefaults, bool withChecks);
explicit C(int* array, size_t size);
};В какой-то момент Вы решаете удалить второй конструктор, и может быть надеетесь, что компилятор укажет Вам на все места, требующие исправления. Но этого не происходит. Из-за неявных преобразований в bool, первый конструктор будет использован везде, где раньше использовался второй.
Однако есть причина, почему люди обычно используют bool для представления флагов. Это единственный встроенный тип, доступный «из коробки» и предназначенный для представления только двух возможных значений.
Перечисления
Чтобы решить указанные проблемы, мы должны располагать типом, отличным от bool, который удовлетворял бы следующим требованиям:
— для каждого флага создается уникальный тип,
— неявные преобразования запрещаются.
C++11 вводит понятие классов перечислений, которые удовлетворяют обоим требованиям. Также мы можем использовать тип bool как базовый тип перечисления; таким образом, мы гарантируем, что перечисление содержит только два возможных значения и имеет размер одного bool. Вначале определяем классы флагов:
enum class WithValidation : bool { False, True };
enum class WithNewEngine : bool { False, True };Теперь мы можем объявить нашу функцию:
void process(WithValidation withValidation,
WithNewEngine withNewEngine);Есть некоторая избыточность в этом объявлении, но зато порядок использования функции теперь такой, какой нужен:
process(WithValidation::False, WithNewEngine::True); // okИ если я поставлю флаги в неправильном порядке, то получу ошибку компиляции из-за несоответствия типов:
process(WithNewEngine::True, WithValidation::False); // ошибка!Каждый флаг имеет уникальный тип, который исправно работает при прямой передаче (perfect forwarding), и Вы никак не сможете расположить параметры в неправильном порядке в объявлениях функций и переопределениях виртуальных методов.
Но использование перечислений в качестве флагов имеет свою цену. Флаги в некоторой мере похожи на значения типа bool, но классы перечислений не имитируют эту схожесть. Неявные преобразования в bool и обратно не работают (и это хорошо), но явные преобразования тоже не работают, и это проблема. Если мы взглянем еще раз на тело функции process, то поймем, что оно не компилируется:
void process(WithValidation withValidation,
WithNewEngine withNewEngine)
{
if (withValidation) // ошибка: нет контекстного преобразования в bool
validate();
// ...
}Мне приходится использовать явное преобразование:
if (bool(withValidation)) // ok
validate();И если мне понадобится логическое выражение с двумя флагами, то оно будет выглядеть еще неадекватнее:
if (bool(withNewEngine) || bool(withValidation))
validate();Кроме того, для экземпляра класса перечисления Вы не можете сделать прямую инициализацию из bool:
bool read_bool();
class X
{
WithNewEngine _withNewEngine;
public:
X()
: _withNewEngine(read_bool()) // ошибка
{}
};Опять придется делать явное преобразование:
class X
{
WithNewEngine _withNewEngine;
public:
X()
: _withNewEngine(WithNewEngine(read_bool())) // ok
{}
};Это можно считать дополнительной гарантией безопасности, но здесь слишком много явных преобразований. В классах перечислений больше «explicit», чем в конструкторах и операторах преобразования, объявленных как «explicit».
tagged_bool
Из-за проблем, возникающих при использовании bool и классов перечислений, мне пришлось сделать свой собственный инструмент, который называется tagged_bool. Вы можете найти его реализацию здесь. Она совсем небольшая. С ее помощью, классы флагов объявляются вот так:
using WithValidation = tagged_bool<class WithValidation_tag>;
using WithNewEngine = tagged_bool<class WithNewEngine_tag>;Вам придется сделать предварительное объявление класса-тэга, такого как «WithValidation_tag». Определение для него писать не нужно. Он используется для создания уникальной специализации шаблона класса tagged_bool. Эта специализация может быть явно преобразована в bool и обратно, а также в другие специализации шаблона tagged_bool, поскольку, как обычно бывает на практике, какой-нибудь bool, передающийся на нижние уровни приложения, становится впоследствии другим флагом с другим именем. Использовать созданные таким образом флаги можно вот так:
void process(WithValidation withValidation,
WithNewEngine withNewEngine)
{
if (withNewEngine || withValidation) // ok
validate();
// ...
}
process(WithValidation{true}, WithNewEngine{false}); // ok
process(WithNewEngine{true}, WithValidation{false}); // ошибкаВот и все. tagged_bool — это часть библиотеки Explicit library, которая содержит несколько инструментов, позволяющих более явно выразить Ваши намерения при проектировании интерфейсов.
От переводчика
У Анджея ранее была другая статья про тэги — «Intuitive interface — Part I» от 5 июля 2013 г. (Part 2 так и не появилась на свет, не ищите). Если вкратце, то там поднималась такая проблема:
std::vector<int> v1{5, 6}; // 2 элемента: {5, 6}
std::vector<int> v2(5, 6); // 5 элементов: {6, 6, 6, 6, 6}Когда поведение конструктора зависит от формы скобочек, это само по себе опасно. Кроме того, это делает непонятным вызывающий код:
std::vector<int> v(5, 6);Что такое 5 и 6? Это будет 5 шестерок или 6 пятерок? Если забыли — идите смотреть документацию.
И хотелось бы иметь еще один конструктор, создающий пустой вектор с заданной capacity: std::vector v(100). К сожалению, конструктор, принимающий один size_t, уже занят — он создает вектор с заданным size’ом, заполненный сконструированными по умолчанию объектами.
Анджей упоминает, что такой порядок вещей не дает в полной мере воспользоваться возможностями прямой передачи, но в комментариях ему разъяснили, что эта проблема решается без всяких тэгов.
Анджей пришел к выводу, что реализация вектора в библиотеке STL не вполне удачная. Было бы куда проще, если бы в его конструкторах использовались тэги:
std::vector<int> v1(std::with_size, 10, std::with_value, 6);Применительно к настоящей статье, это выглядело бы так:
process(withValidation, true, withNewEngine, false); // ok
process(withNewEngine, true, withValidation, false); // ошибкаРазница в том, что тэг и значение теперь объединены в один объект. В статье «Competing constructors» от 29 июля 2016 г. Анджей мимоходом написал, что ему не нравится идея такого объединения.
vector<int> w {with_size{4}, 2}; // {2, 2, 2, 2}
vector<int> x {with_size{4}}; // {0, 0, 0, 0}
vector<int> y {with_capacity{4}}; // {}Теперь это уже не тэги, а полноценные объекты. Кому-нибудь может прийти в голову положить их в контейнер:
vector<with_size> w {with_size{4}, with_size{2}};Поведение этого кода опять зависит от формы скобочек. Что за радость была вводить тэги, если мы снова вернулись к той же проблеме? По крайней мере, простые тэги вряд ли кому-нибудь захочется хранить в контейнере. Ведь они могут иметь только одно значение.
С bool, однако, эта угроза не так страшна. Конструкторы контейнеров STL не принимают bool, иначе как в составе initializer_list’ов. Видимо, поэтому Анджей и решился в этот раз объединить тэг и значение.
Напоследок приведу перевод нескольких комментариев к статье.
Комментарии
kszatan
February 17, 2017 at 11:36 am
Я бы в первую очередь подумал об избавлении от всех этих ��лагов и вывел бы код для подтверждений (validations) и нового/старого движка (new/old engine) в отдельные классы, чтобы передавать их как аргументы. Функция «process» и так уже делает слишком много.
Andrzej Krzemieński
February 17, 2017 at 12:03 pm
В простых случаях отказ от любых флагов действительно может оказаться лучшим выбором. Но когда решение установить флаг принимается несколькими уровнями выше в стеке вызовов, такой рефакторинг может оказаться неосуществимым или непрактичным.
int main()
{
Revert revert_to_old_functionality {read_from_config()};
Layer1::fun(revert_to_old_functionality)
}
void Layer1::fun(Revert revert)
{
// something else...
Layer2::fun(revert);
};
void Layer2::fun(Revert revert)
{
// something else...
Layer3::fun(revert);
};
void Layer3::fun(Revert revert)
{
// something else...
if (revert)
do_poor_validation();
else
do_thorough_validation();
};===Конец треда===
micleowen
February 17, 2017 at 10:41 pm
class C
{
explicit C(bool withDefaults, bool withChecks);
explicit C(int* array, size_t size);
};«explicit» используется в конструкторах с одним параметром.
Andrzej Krzemieński
February 20, 2017 at 8:22 am
Есть основательная причина объявлять почти все конструкторы как «explicit», а не только конструкторы с 1 аргументом (особенно с выходом стандарта C++11). Иногда даже конструктор по умолчанию лучше объявить как «explicit». Кому интересно, советую обратиться вот к этой статье .
===Конец треда===
ARNAUD
February 18, 2017 at 6:39 pm
«Неявные преобразования в bool и обратно не работают (и это хорошо), но явные преобразования тоже не работают, и это проблема»
Не понимаю, что в этом плохого:
if(withValidation == WithValidation::True)Сначала Вы используете общеизвестную возможность языка, и Ваш код прекрасно читается и понимается всеми специалистами по С++. Потом Вы переходите к использованию специального шаблона для автоматического преобразования в bool и обратно? Меня это не убеждает.
И еще. Представьте, что через какое-то время один из параметров перестанет быть bool и сможет принимать значения no_engine, engine_v1, engine_v2… Класс перечисления позволяет сделать такое расширение естественным путем, в отличие от Вашего tagged_bool.
Andrzej Krzemieński
February 20, 2017 at 8:36 am
Вы подняли два вопроса.
1. Выбор между
if (withValidation || withNewEngine)и
if (withValidation == WithValidation::True
|| withNewEngine == WithNewEngine::True)И, в случае использования пространств имен:
if (withValidation == SomeNamespace::WithValidation::True
|| withNewEngine == SomeNamespace::WithNewEngine::True)Для меня это компромисс между желаемым уровнем безопасности и удобством использования. Мой личный выбор — что-то безопаснее bool, но не такое многословное как классы перечислений. Видимо, Ваш компромисс лежит ближе к классам перечислений.
2. Возможность добавить третье состояние
Если Вы предвидите, что в будущем Вам может понадобиться третье состояние, то классы перечислений и правда могут быть предпочтительнее. А могут и не быть. Потому что, когда Вы добавляете третье состояние, все Ваши if’ы продолжают исправно компилироваться, хотя Вы, может быть, желаете их отредактировать, чтобы добавить проверку третьего состояния.
По моему опыту, эти ��лаги используются как временные решения, и их дальнейшее развитие не в том, чтобы добавить третье состояние, а в том, чтобы избавиться от двух имеющихся. Например, я улучшаю какую-то часть программы, но в течение пары месяцев хочу дать пользователям возможность переключиться обратно на старую реализацию, на случай, если я что-то недосмотрел, и улучшение только все испортит. Если после пары месяцев все пользователи остались довольны, я удаляю поддержку старой реализации и избавляюсь от флага.
===Конец треда===
mftdev00
March 13, 2017 at 1:05 pm
Я вообще не люблю флаги. Они противоречат принципу единственной ответственности. Делай что-то, если true, делай что-то другое, если false…
Andrzej Krzemieński
March 13, 2017 at 1:10 pm
Согласен. Везде, где это возможно, нужно обходиться без флагов.
===Конец треда===
SebB
March 21, 2017 at 6:09 pm
Можно ли вместо явного удаления конструкторов для каждого типа:
constexpr explicit tagged_bool (bool v) : value {v} {}
constexpr explicit tagged_bool (int) = delete;
constexpr explicit tagged_bool (double) = delete;
constexpr explicit tagged_bool (void*) = delete;…просто удалить их для всех типов (кроме bool) разом?
constexpr explicit tagged_bool (bool v) : value {v} {}
template <typename SomethingOtherThanBool>
constexpr explicit tagged_bool(SomethingOtherThanBool) = delete;Andrzej Krzemieński
March 22, 2017 at 7:32 am
Я просто не учел такую возможность, когда разрабатывал интерфейс. Может, и полезно было бы это добавить. Но теперь, когда Вы это предложили, я вижу один случай, где это имело бы отрицательный эффект: кто-то может использовать свой собственный (безопасный) логический тип с неявным преобразованием в bool. В этом случае, нам, может быть, нужно позволить этому типу работать с tagged_bool.
