[{"content":"You have just written a new library. It\u0026rsquo;s an incredible new library which will revolutionise the way that we handle widgets forever. But there is just one problem - the user has to opt into it themselves. For one reason or another you can\u0026rsquo;t assume whether a class is compatible from the code itself, you need the user to make an explicit check, and you need to provide the API for them to do so.\ntemplate\u0026lt;opted_in T\u0026gt; constexpr Widget widgetiser(T\u0026amp;\u0026amp; user_provided_class); So, how do you go about it? How do you make the opted_in concept? There are quite a few ways in C++ which have varying degrees of success, and a small minefield of things which can go wrong. Let\u0026rsquo;s examine some of the options, considering how convenient they are, how easy to understand they are, and how easy it is to customise them based on the user class.\nClass typedefs and named tags The classic case which standard library iterators have been using since the standard library came into being. As part of your public API you can expose a name in the form of a typedef for your library to latch onto. Consider:\n//Library code template\u0026lt;typename T\u0026gt; concept opted_in = requires{ typename T::is_widget; }; //User code struct user_class{ using is_widget = void; //... }; Sorted. Except, how do we make this conditional? If the user class is a template, how do we opt in for only a certain subset of types? Well, we can encode a special type, and require that T::is_widget is the same type as yes_i_definitely_opted_in_t; but this is hardly ergonomic, particularly for the general case where you want to unconditionally opt in. You could try to invert it so that exclusively in the template case, T::is_widget is actually_i_opt_out_t for the cases where your class is not a suitable widget; but then you are stacking up more and more negative conditions to try to describe your code. You could make it a constexpr bool but then you require an explicit opt-in in the unconditional case; and if you\u0026rsquo;re using multiple opt-in libraries you tightly couple those to the definition of your class by introducing reachable, useable names whose meanings are largely irrelevant to what the class is actually supposed to be. Equally, not everything can have member typedefs, only classes. If you want to make a particular enum, or function or universal constant valid for widgetisation then you\u0026rsquo;re out of luck.\nAnd there is a simple question here - how do you opt third party classes into your library for use in your code? You might not be able to reasonably edit the definition and maintain a fork from its original source for the sake of easier widgeting.\nAn opt-in template boolean This is the more modern approach which ranges-v3 and std::ranges tend to follow (e.g. std::ranges::enable_borrowed_range). Rather than read something tightly coupled inside the class definition, you provide a template boolean, say enable_widgetising, which you then specialise to true for all the classes which follow it. So:\n//Library code template\u0026lt;typename T\u0026gt; constexpr inline bool enable_widgetising = false; template\u0026lt;typename T\u0026gt; concept opted_in = enable_widgetising\u0026lt;T\u0026gt;; //Arguable about whether a concept is necessary //User code struct user_class{ //... }; template\u0026lt;\u0026gt; constexpr inline bool enable_widgetising\u0026lt;user_class\u0026gt; = true; There we go. Now your user classes have an entirely bespoke and entirely unique-to-your-library name they can customise to true. You can also selectively apply this boolean to class templates in user code by constraining the boolean, so\n//Everyone knows that integer widgets are the only true widgets template\u0026lt;typename T\u0026gt; requires std::integral\u0026lt;T\u0026gt; constexpr inline bool enable_widgetising\u0026lt;user_class\u0026lt;T\u0026gt;\u0026gt; = true; And this will compile and behave as you reasonably expect. Since we have decoupled the opt-in mechanism from the class definition, it is also trivial to enable your library for third party code. There is a catch here, however. Every time the compiler attempts to evaluate enable_widgetising\u0026lt;user_class\u0026gt;, it must come to the same result otherwise your program is ill-formed, no diagnostic required (IFNDR) - a very special kind of wrong where the compiler is no longer required to produce results which are logical or even deterministic through successive builds of the program. To give an example of this, consider two TUs:\n//TU A #include \u0026#34;user_class.hpp\u0026#34; //Everyone knows that user_class is not a widget... static_assert(not enable_widgetising\u0026lt;user_class\u0026gt;); //TU B #include \u0026#34;user_class.hpp\u0026#34; #include \u0026#34;widgetiser.hpp\u0026#34; //...But I can shortcut the logic in my current ticket by making it behave like one template\u0026lt;\u0026gt; constexpr inline bool enable_widgetising\u0026lt;user_class\u0026gt; = true; [temp.expl.spec] requires that the specialisation we provide must be reachable from every use in every TU. Neither TU can see the other, so each will compile happily with its own answer. At link time, the linker will pick one answer and discard the other, and the behaviour of the rest of the program will then depend on link order and optimisation level; which in turn will kick up bugs which appear in one file but are actually the result of compiling another, potentially at any time in the past.\nThis can be avoided by still coupling the boolean specialisation to your definition, ensuring that every TU which can see user_class can also see that enable_widgetising is set, if indeed it is.\nIf this seems a little abstract and unlikely to present a problem to you, there is a much more pressing ergonomic concern which makes this kind of boolean unideal - where you place it. Up to this point, to simplify examples, I have not used namespaces. But let\u0026rsquo;s fix that:\nnamespace library{ //... template\u0026lt;typename T\u0026gt; constexpr inline bool enable_widgetising = false; } namespace user{ class user_class {}; //We can\u0026#39;t specialise enable_widgetising here - we\u0026#39;re in the wrong namespace } //So we have to reopen our library namespace. namespace library{ template\u0026lt;\u0026gt; constexpr inline bool enable_widgetising\u0026lt;user::user_class\u0026gt; = true; } But here is the problem. It\u0026rsquo;s not uncommon for a C++ code file to contain multiple classes. It is unthinkably silly to close whatever namespace they\u0026rsquo;re in after each definition, open up the library namespace to customise, close that namespace, and then open up the userspace namespace for the next class definition. So what often ends up happening is that these template booleans drift their way down to the bottom of the file after everything else. Aside from their being out of sight (and therefore out of mind) opening the door to forgetting to update them when you play with the class, you get yourself into a bit of a pickle if any of the classes want to be able to widgetise before you reach the boolean. Be it an inline function body, or use of a previously-declared class, or even just the fact you want to widgetise a plain old class template, you are left in the unfortunate position which might mean that the opt-in to enable widgetising hasn\u0026rsquo;t yet been reached, and you are stuffed.\nA consteval function opt-in reached via ADL So, you think, the problem with a boolean is that it\u0026rsquo;s an object in a different namespace from your own. But, being a good developer, you recall that argument-dependent lookup has long been a basis for customisation points in classes in order to avoid this very problem. And since you are defining a function to determine opt-in eligibility you can have an easy time writing freeform code rather than trying to round-trip it through a concept. So, let\u0026rsquo;s take a look at what we can do:\n//Library code consteval bool enable_widgetising(auto) = delete; //More on this later template\u0026lt;typename T\u0026gt; concept opted_in = requires(T){ { enable_widgetising(std::type_identity\u0026lt;T\u0026gt;{}) } -\u0026gt; std::same_as\u0026lt;bool\u0026gt;; } \u0026amp;\u0026amp; enable_widgetising(std::type_identity\u0026lt;T\u0026gt;{}); //User code class user_class{}; consteval bool enable_widgetising(std::type_identity\u0026lt;user_class\u0026gt;){ return true; } In short, we require that it is possible to call some function enable_widgetising with some tag type which represents our desired type, and then we require that when we do call it the result is true. In this case we use std::type_identity, but so long as it is a type which will cause ADL to search in the namespace of your user_class (this is guaranteed for type_identity\u0026lt;user_class\u0026gt; via [basic.lookup.argdep] as it is for other templates with user_class as a template argument) then there is no messing with namespaces needed. It\u0026rsquo;s trivial to customise the behaviour based on your class, if it is a template, within the function and in a way which makes it as unobscured from the user as any approach discussed so far. You get full flexibility. You can even place it inside your class definition as a hidden friend and use your function inside the class itself, across namespaces, seamlessly:\nnamespace library{ consteval bool enable_widgetising(auto) = delete; template\u0026lt;typename T\u0026gt; concept opted_in = requires { { enable_widgetising(std::type_identity\u0026lt;T\u0026gt;{}) } -\u0026gt; std::same_as\u0026lt;bool\u0026gt;; } \u0026amp;\u0026amp; enable_widgetising(std::type_identity\u0026lt;T\u0026gt;{}); template\u0026lt;opted_in T\u0026gt; void widgetise(T) {} } namespace user{ template\u0026lt;typename T\u0026gt; class user_class{ public: friend consteval bool enable_widgetising(std::type_identity\u0026lt;user_class\u0026gt;){ return true; } void some_function(){ library::widgetise(*this); } }; //And of course the conditional case is simple template\u0026lt;typename I\u0026gt; class user_class_v2{ public: friend consteval bool enable_widgetising(std::type_identity\u0026lt;user_class_v2\u0026gt;){ return std::integral\u0026lt;I\u0026gt;; } }; } Godbolt here\nThe deleted enable_widgetising(auto) function is a poison pill to contain the lookup to just relevant places when checking the concept. When the concept check tries to require that enable_widgetising(std::type_identity\u0026lt;T\u0026gt;) is true, it checks namespace std (because that\u0026rsquo;s where type_identity lives), and the namespace in which T is declared. If it doesn\u0026rsquo;t find anything named enable_widgetising, it then \u0026ldquo;steps outwards\u0026rdquo; and checks the library namespace, where it will encounter the deleted function and, since it can\u0026rsquo;t be called, will mean that the concept check fails.1 The fact that we use auto is an overload resolution trick - if more than one match is found in the library namespace, then a function which is explicitly declared as taking an argument of some std::type_identity for some type will rank higher than an unspecialised template and be chosen first; such that a class in the library\u0026rsquo;s own namespace being opted in will still be opted in, but the compiler will never step further out and consider other namespaces or the global namespace when looking for a match for enable_widgetising.\nThere are a few caveats to note - this change is not immune to the same IFNDR concern mentioned with a template boolean. If you opt to place the opt-in function outside of the class definition, then the result when checking if a class has opted in must be the same at all places it is checked in all TUs. This is also a minimal example and you may need judicious use of remove_cvref_t and similar in your library definitions if your function accepts cv-qualified or reference types.\nThis seems to have answered some of the problems with the other methods - we are free to place the opt-in mechanism in a few different places without it interfering with our work, but for the case of third party types we still need to open up their namespaces to place the declaration for ADL to work. But it can still be improved. It\u0026rsquo;s more lines of code both at the library and user level and starts to require understanding of slightly more niche language mechanisms to fully utilise. Perhaps there\u0026rsquo;s another way.\nAn annotation on the class definition Among the reflection tools added in C++26, we received annotations. For those unfamiliar, an annotation is syntactically similar to an attribute, except that it is prefixed with = and can be any structural type constant expression. With reflection we can reflect on the resulting values of that expression. So, let\u0026rsquo;s construct a basic example:\n//Library code struct enable_widgetising_t {}; constexpr inline enable_widgetising_t enable_widgetising {}; template\u0026lt;typename T\u0026gt; concept opted_in = not annotations_of_with_type(^^T, ^^enable_widgetising_t).empty(); //User code class [[=enable_widgetising]] user_class{ }; Nice and simple. The annotation [[=enable_widgetising]] is an expression whose underlying type is enable_widgetising_t. We use the C++26 reflection metafunction std::meta::annotations_of_with_type (here called via ADL) to see if T has any annotations of type enable_widgetising_t, and our concept works.\nBut, we ask the same question as on everything else - how do we customise this, for example if our class is a template of which only certain specialisations should widgetise? Let\u0026rsquo;s start by considering what we want our final syntax to be. I think that the code in the above example is ideal for the non-conditional variant, and to add a boolean condition, we should follow the way of the C++ keywords so that [[=enable_widgetising(some_condition)]] will enable widgetising if and only if some_condition is met.\nFortunately, this is fairly simple. We can add a function call operator to our tag type so that both enable_widgetising and enable_widgetising(condition) are valid expressions. But there is one catch - we can\u0026rsquo;t just check if the type of the annotation is enable_widgetising_t, since we can\u0026rsquo;t return types as types; so instead we attach our boolean state so that we can pass out something which is true or false, and which is the same type as the unconditional case.\n//Library code struct enable_widgetising_t{ bool enabled = true; consteval enable_widgetising_t operator()(bool b) const{ return {b}; } }; constexpr inline enable_widgetising_t enable_widgetising{}; template\u0026lt;typename T\u0026gt; consteval bool opted_in_via_annotation(){ for(auto annotation : annotations_of_with_type(^^T, ^^enable_widgetising_t)){ if(extract\u0026lt;enable_widgetising_t\u0026gt;(annotation).enabled){ return true; } } return false; }; template\u0026lt;typename T\u0026gt; concept opted_in = opted_in_via_annotation\u0026lt;T\u0026gt;(); //Again arguable that a concept is needed //User code template\u0026lt;typename I\u0026gt; class [[=enable_widgetising(std::integral\u0026lt;I\u0026gt;)]] user_class{ }; In short, we carry a boolean which will default to true unless otherwise perturbed, and so we are enabled by default. When we pass in a condition which is false, we return a tag with the boolean also set to false and when we examine annotation we can query whether it is enabled or disabled. Simple.\nThis approach has returned us back to where we started - if the annotation is tightly coupled to the class definition, then third party classes cannot pick it up. Except, they can if you\u0026rsquo;re willing to get your hands dirty. The definition of your class can only exist in your immutable third-party headers, but declarations of it can exist anywhere. Declarations can carry annotations, and when the annotations on an object are queried then the result is the union of the annotations on all declarations and definitions the compiler can see. gcc seems to counter such hackery by ignoring annotations on a class declaration which appears after its definition. But it doesn\u0026rsquo;t ignore declarations before the definition. So if you\u0026rsquo;re willing to hold your nose and do something like this:\n#include \u0026#34;widgetiser.hpp\u0026#34; class [[=enable_widgetising]] user_class; #include \u0026#34;user_class.hpp\u0026#34; Now, I am definitely not recommending that you actually do this in production code. This is a quirk to hack around a very reasonable restriction and if you do this in your code then there\u0026rsquo;s a good chance it\u0026rsquo;ll break, either from the IFNDR reasons discussed previously or the fact that user_class might not be a class but instead a type alias or some other construct.\nWhat\u0026rsquo;s more interesting about the annotation approach is that now we have the annotation looking up named types, it\u0026rsquo;s easy to generalise the machinery:\n//Shared opt-in machinery template\u0026lt;typename Unique\u0026gt; struct opt_in_t { bool enabled = true; consteval opt_in_t operator()(bool b) const { return {b}; } }; template\u0026lt;auto\u0026amp; Key\u0026gt; consteval bool opted_in_via_annotation(std::meta::info cls) { using K = std::remove_cvref_t\u0026lt;decltype(Key)\u0026gt;; for (auto annotation : annotations_of(cls)) if (is_same_type(remove_cvref(type_of(annotation)), ^^K)){ return extract\u0026lt;K\u0026gt;(annotation).enabled; } return false; } //What we need to define for widgets struct widgetiser_tag; constexpr inline opt_in_t\u0026lt;widgetiser_tag\u0026gt; enable_widgetiser{}; template\u0026lt;typename T\u0026gt; concept opted_in = opted_in_via_annotation\u0026lt;enable_widgetiser\u0026gt;(^^T); //And user code remains class [[=enable_widgetiser(some-condition)]] user_class {}; Where we define a widgetiser_tag unique type to make sure that our specialisation of opt_in_t is itself a unique type, distinct from other specialisations; or rather, so that opting in to one thing does not opt you into everything by mistake. An alternative route is to use a reflection of the thing you are opting into:\ntemplate\u0026lt;std::meta::info Unique\u0026gt; struct opt_in_t { bool enabled = true; consteval opt_in_t operator()(bool b) const { return {b}; } }; //Opt-in function is unchanged from previous example namespace widgetiser_lib { constexpr inline opt_in_t\u0026lt;^^widgetiser_lib\u0026gt; enable_widgetiser{}; template\u0026lt;typename T\u0026gt; concept opted_in = opted_in_via_annotation\u0026lt;enable_widgetiser\u0026gt;(^^T); } //And user code is unchanged class [[=widgetiser_lib::enable_widgetiser(some-condition)]] user_class {}; Note that we can\u0026rsquo;t reflect on our opt-in function directly, as this will create a circular dependency - its constraint needs enable_widgetiser to exist, and enable_widgetiser would need the constraint to exist to reflect on a proper declaration. But, as you can reflect on pretty much anything in C++26, a sensible alternative might be the library namespace you are opting into.\nThis is probably as far as it is sensible to take this, but what if we could take it further? What if it\u0026rsquo;s just too onerous to define a unique new type when opting in, or deciding which entity to reflect on, for the sake of satisfying some template pedantry. What we\u0026rsquo;d need is a way for the compiler to mint an entirely unique type on-demand with some kind of unutterable name. Well, fortunately C++ has just the feature. What if instead, we wrote:\nconstexpr inline opt_in_t\u0026lt;decltype([]{})\u0026gt; enable_widgetiser{}; And relied on the fact that a lambda, even one which is token-identical to another lambda, will always generate an entirely bespoke type? Well, this would probably work in isolated test cases, but lambdas are surrounded by a cloud of uncertainty in the standard when it comes to exactly whether a lambda definition which is included in multiple TUs refers to the same entity.\nThe important question to ask here is simple - is this enable_widgetiser the same type in every TU it\u0026rsquo;s included into, or is it a different one? As far as I can tell, the standard\u0026rsquo;s answer is a somewhat sheepish maybe. [basic.def.odr] seems to suggest that such lambdas in the definition of an inline entity have the same closure type, which would appear to cover us. However, a note in that clause explicitly tells us that the entity is still declared in multiple TUs, that [basic.link] applies to those declarations, and that lambdas appearing in the type of an entity can result in each declaration having a different type. [basic.link] requires that every declaration of a variable gives it the same type, otherwise the program is IFNDR. If the type of the lambda matches everywhere then our trick works; but if not then our program is ill-formed, no diagnostic required and those last three words mean that we might not find out until the least opportune time.\nEntertainingly, if we define our constant instead like this:\nconstexpr inline auto enable_widgetiser { opt_in_t\u0026lt;decltype([]{})\u0026gt; {}}; Then the standard is surprisingly clear that, due to the lambda being in the initializer of an inline variable, this is unambiguously well-defined everywhere, per a specific paragraph in [basic.def.odr].\nEqually, you may have the bright idea that typing out decltype([]{}) every time is too much like hard work and that you\u0026rsquo;d instead rather just use it as the default template argument for opt_in_t. Here the standard is much clearer. The same [basic.def.odr] which might save us for the type definition case and which does save us in the initializer case treats a default template argument as if it were written inside the definition, so the created closure is required to have the same type in every TU. But a later point explicitly excludes lambdas in default template arguments from being treated as the same entity across TUs. This seems to reach a confusing conclusion that they are required to match while also guaranteeing that they don\u0026rsquo;t, and that your program is IFNDR.\nHowever, the initializer case being well-defined does not inherently mean your program will work. Compilers quite notoriously have problems maintaining the exact letter of the standard in fringe lambda edge-cases such as this one, and may silently break your program anyway. Indeed we can see a conformance issue just by throwing this example into godbolt. gcc correctly gives the initializer case the same definition across TUs, but Clang silently gives the closure internal linkage, meaning each type is unique even if they report the same typeid name. It\u0026rsquo;s most likely worth the strain of defining a new type or deciding on something to reflect on when defining the opt-in annotation - those at least are always guaranteed to be consistent in every TU and implementations consistently get it right.\nOverall, the annotation idea solves many of our problems with just how flexible annotations can be, and how easy they are to append onto almost anything in the language. But if you go down the library route in your own code, it\u0026rsquo;s far more sensible to define a tag type or use a reflection as the unique key than it is to try to solve that problem with lambda hijinks.\nOpting back out again The dark side of template metaprogramming is a pathway to many abilities the Committee consider to be unnatural. I refer of course to the black magic known as stateful metaprogramming - there are ways and means to inject state into the compiler as it is parsing and processing your code, and ways to exploit that state to effect some program logic. But in case it\u0026rsquo;s not clear everything in this section comes with a huge disclaimer - do not do this in any real production code. A lot of it is entirely IFNDR and even if parts are somehow well-defined by the letter of the standard, the committee have gone on the record to say that they don\u0026rsquo;t like that this is possible and want to find a way to forbid it. Also your compiler may spuriously decide to break it if it caches a definition differently between builds. While there are many ways to go about this, the classic method is:\nFriend Injection The idea behind friend injection is simple. You declare, but don\u0026rsquo;t define, a friend function inside of some template. Then, later on, you can provide a definition of that function in another template. Instantiating the template will define the friend function, and the definition outlives the instantiation. Consider:\ntemplate\u0026lt;int N\u0026gt; struct counter_slot { //Declare our friend friend constexpr auto is_taken(counter_slot\u0026lt;N\u0026gt;); }; template\u0026lt;int N\u0026gt; struct take_slot { //And define it here friend constexpr auto is_taken(counter_slot\u0026lt;N\u0026gt;) { return true; } }; //Each call needs a fresh specialisation, or the compiler can cache previous results //So we use a lambda for uniqueness template\u0026lt;int N = 0, auto = []{}\u0026gt; consteval int next_id() { if constexpr (requires { is_taken(counter_slot\u0026lt;N\u0026gt;{}); }) { return next_id\u0026lt;N + 1\u0026gt;(); } else { static_cast\u0026lt;void\u0026gt;(take_slot\u0026lt;N\u0026gt;{}); return N; } } The instantiation of counter_slot\u0026lt;0\u0026gt; quietly declares a unique is_taken function at namespace scope, invisible to normal lookup. What\u0026rsquo;s more, it has an auto return type so it cannot be called until the compiler sees its definition, and take_slot will provide that definition only after take_slot\u0026lt;0\u0026gt; is instantiated. Then ADL lets the call is_taken(counter_slot\u0026lt;N\u0026gt;{}) find the declaration. So when the function checks requires { is_taken(counter_slot\u0026lt;N\u0026gt;{}); }, if take_slot\u0026lt;N\u0026gt; has already been instantiated elsewhere then is_taken(counter_slot\u0026lt;N\u0026gt;{}) is callable and the requires expression comes up true. If take_slot\u0026lt;N\u0026gt; has not been instantiated anywhere, the requires expression attempts to call an uncallable function, comes out as false, and the function then instantiates take_slot\u0026lt;N\u0026gt;. As such, every time you call next_id, the compiler will have to instantiate a new take_slot specialisation, which in turn gives you an entirely new N.\nBut how does this relate to opting out? Well, it\u0026rsquo;s not a large logical leap to consider that maybe if the latest N is even then we can widgetise and if it is odd then we can\u0026rsquo;t; we can wrap the counter up in functions named opt_in and opt_out, and go from there. And it\u0026rsquo;s not that much harder to write the code, but the requirement of uniqueness does start to go viral. The compiler is perfectly within its rights to cache the result of multiple templates with the same arguments, as the intent of the standard is that these should universally provide the same answer every time. So, we need to push lambdas everywhere to enforce a unique evaluation. But, it can be done:\n#include \u0026lt;print\u0026gt; #include \u0026lt;type_traits\u0026gt; //Same as before, except we introduce a typename T for the user class template\u0026lt;typename T, int N\u0026gt; struct toggle_slot { friend constexpr auto is_taken(toggle_slot\u0026lt;T, N\u0026gt;); }; template\u0026lt;typename T, int N\u0026gt; struct take_toggle { friend constexpr auto is_taken(toggle_slot\u0026lt;T, N\u0026gt;) { return true; } }; template\u0026lt;typename T, int N = 0, auto = []{}\u0026gt; consteval int toggles_so_far() { if constexpr (requires { is_taken(toggle_slot\u0026lt;T, N\u0026gt;{}); }) { return toggles_so_far\u0026lt;T, N + 1\u0026gt;(); } else { //Except we are currently just querying so we don\u0026#39;t instantiate a new specialisation yet return N; } } //An even-numbered slot opts in, an odd-numbered slot opts back out template\u0026lt;typename T, auto = []{}\u0026gt; consteval bool widgetising_enabled() { return toggles_so_far\u0026lt;T\u0026gt;() % 2 == 1; } //So our opt-in/opt-out simply handles the instantiation for us to make sure the number is suitably //even or odd after calling, with a static_assert to catch out-of-order calls. template\u0026lt;typename T, auto = []{}\u0026gt; consteval bool opt_in_to_widgetising() { constexpr int next_slot = toggles_so_far\u0026lt;T\u0026gt;(); static_assert(next_slot % 2 == 0, \u0026#34;already opted in\u0026#34;); static_cast\u0026lt;void\u0026gt;(take_toggle\u0026lt;T, next_slot\u0026gt;{}); return true; } template\u0026lt;typename T, auto = []{}\u0026gt; consteval bool opt_out_of_widgetising() { constexpr int next_slot = toggles_so_far\u0026lt;T\u0026gt;(); static_assert(next_slot % 2 == 1, \u0026#34;not opted in\u0026#34;); static_cast\u0026lt;void\u0026gt;(take_toggle\u0026lt;T, next_slot\u0026gt;{}); return true; } struct Widget {}; //The concept takes a fresh lambda too, so every check is a new atomic constraint template\u0026lt;typename T, auto Fresh\u0026gt; concept opted_in = widgetising_enabled\u0026lt;std::remove_cvref_t\u0026lt;T\u0026gt;, Fresh\u0026gt;(); //As does your library function template\u0026lt;typename T, auto Fresh = []{}\u0026gt; requires opted_in\u0026lt;T, Fresh\u0026gt; constexpr Widget widgetiser(T\u0026amp;\u0026amp;) { return {}; } template\u0026lt;typename T, auto = []{}\u0026gt; constexpr bool can_widgetise = requires { widgetiser(T{}); }; struct user_class {}; constexpr bool before_opting_in = can_widgetise\u0026lt;user_class\u0026gt;; static_assert(opt_in_to_widgetising\u0026lt;user_class\u0026gt;()); constexpr bool after_opting_in = can_widgetise\u0026lt;user_class\u0026gt;; static_assert(opt_out_of_widgetising\u0026lt;user_class\u0026gt;()); constexpr bool after_opting_out = can_widgetise\u0026lt;user_class\u0026gt;; static_assert(opt_in_to_widgetising\u0026lt;user_class\u0026gt;()); constexpr bool after_opting_back_in = can_widgetise\u0026lt;user_class\u0026gt;; int main() { std::println(\u0026#34;Before opting in: {}\u0026#34;, before_opting_in); std::println(\u0026#34;After opting in: {}\u0026#34;, after_opting_in); std::println(\u0026#34;After opting out: {}\u0026#34;, after_opting_out); std::println(\u0026#34;After opting back in: {}\u0026#34;, after_opting_back_in); } Godbolt here.\nTen years ago this was the hot trick to talk about at C++ cocktail parties, but times have moved on, C++26 is here, and there\u0026rsquo;s a whole new language waiting for ways to break things:\nReflection On the face of it, it\u0026rsquo;s trivial to get inconsistent answers out of C++26 reflection - it comes with a function to count how many entities can be seen at that point in the TU, and while it doesn\u0026rsquo;t have a full suite of generative facilities it also comes with a function to inject new definitions in the form of define_aggregate. Indeed we can knock out a rhyme of our friend injection technique in a few metafunctions:\n//Declare but don\u0026#39;t define a class this time template\u0026lt;int N\u0026gt; struct history_slot; //Get a (still undefined) reflection of history_slot\u0026lt;index\u0026gt; consteval std::meta::info slot_for(int index) { return substitute(^^history_slot, {std::meta::reflect_constant(index)}); } //And iterate up the values of index until we find the next undefined value consteval int next_id() { int index = 0; while (is_complete_type(slot_for(index))) { ++index; } return index; } //And separate defining the next slot //We must define the injection in a consteval block consteval void take_slot() { define_aggregate(slot_for(next_id()), {}); } static_assert(next_id() == 0); consteval { take_slot(); } static_assert(next_id() == 1); Godbolt here\nThis has a few advantages over friend injection - the code is much more transparent about what it\u0026rsquo;s doing and, minus the consteval block requirement for define_aggregate calls, isn\u0026rsquo;t relying on implicit quirks of the standard which require multiple citations to follow. And it is of course possible to reengineer the opt-in/opt-out mechanism with this functionality. But reflection lets us take this a step further - because you can now wire up almost anything to anything else, what if we wanted to add metadata about the opt-in and opt-out at the point where it happens?\n//Create the toggle with a reflectable reason //why we toggled it struct opt_in_t { bool enabled = true; const char* reason = \u0026#34;\u0026#34;; }; namespace widgetising { //Same mechanism as before with a type parameter for our user class template\u0026lt;typename T, int N\u0026gt; struct history_slot; consteval std::meta::info slot_for(std::meta::info cls, int index) { return substitute(^^history_slot, {cls, std::meta::reflect_constant(index)}); } consteval int entries(std::meta::info cls) { int index = 0; while (is_complete_type(slot_for(cls, index))) { ++index; } return index; } consteval void record(std::meta::info cls, opt_in_t opt_in) { auto annotation = std::meta::reflect_constant(opt_in); define_aggregate(slot_for(cls, entries(cls)), {data_member_spec(^^int, {.name = \u0026#34;entry\u0026#34;, .annotations = {annotation}})}); } //Each slot holds a single member, annotated with the opt_in_t it records consteval opt_in_t entry(std::meta::info cls, int index) { auto member = nonstatic_data_members_of(slot_for(cls, index), std::meta::access_context::unchecked())[0]; return extract\u0026lt;opt_in_t\u0026gt;(annotations_of(member)[0]); } //The most recent entry wins, and no entries means not opted in consteval bool enabled(std::meta::info cls) { int count = entries(cls); return count \u0026gt; 0 \u0026amp;\u0026amp; entry(cls, count - 1).enabled; } } struct user_class {}; constexpr bool before_opting_in = widgetising::enabled(^^user_class); consteval { widgetising::record(^^user_class, {true, std::define_static_string(\u0026#34;initial support\u0026#34;)}); } constexpr bool after_opting_in = widgetising::enabled(^^user_class); consteval { widgetising::record(^^user_class, {false, std::define_static_string(\u0026#34;broken by v2\u0026#34;)}); } constexpr bool after_opting_out = widgetising::enabled(^^user_class); //Works just as well on a class we could never edit consteval { widgetising::record(^^std::string, {true, std::define_static_string(\u0026#34;third party\u0026#34;)}); } int main() { std::println(\u0026#34;Before opting in: {}\u0026#34;, before_opting_in); std::println(\u0026#34;After opting in: {}\u0026#34;, after_opting_in); std::println(\u0026#34;After opting out: {}\u0026#34;, after_opting_out); std::println(\u0026#34;std::string: {}\\n\u0026#34;, widgetising::enabled(^^std::string)); std::println(\u0026#34;History of user_class:\u0026#34;); template for (constexpr int index : std::define_static_array(std::views::iota(0, widgetising::entries(^^user_class)))) { constexpr opt_in_t opt_in = widgetising::entry(^^user_class, index); std::println(\u0026#34; {}: {:\u0026lt;5} ({})\u0026#34;, index, opt_in.enabled, opt_in.reason); } } Godbolt here.\nAnd here we are - you don\u0026rsquo;t need to thread lambdas throughout the code to trick the compiler into giving a fresh specialisation, you can opt any class in or out, regardless of whether you own it. More interestingly, we can persuade the compiler to embed comments on why we\u0026rsquo;re doing what we\u0026rsquo;re doing in the code as we are doing it, and query them later when we try to figure out the history. Indeed, this is an interesting mechanism quite apart from the breaking of opt-ins and opt-outs and horrible IFNDR tricks the rest of this section gives.\nConclusion So where does this leave us? I chose the topic of this post not just because it\u0026rsquo;s mildly interesting code, but because in many ways it\u0026rsquo;s a microcosm of the fashions of template programming over the years. C++ has gone from needing to inject an arbitrary name into your class and hope for the best, to methods where you can inject an arbitrary class under a name and see what happens. But in more practical terms:\nThe chief contention in deciding on a good opt-in mechanism is between tightly coupling the opt-in to the class definition (and therefore blocking third party code), and the potential IFNDR issues which come from detaching the opt-in. This is a solvable problem, but requires you to be very careful about how such code enters all TUs in your program. C++26 annotations are probably the best answer for the tightly coupled approach - it\u0026rsquo;s trivial to make their names clear, make their effect conditional, and even generalise and reuse the code to do so as library code. Stateful metaprogramming exists. It allows for all sorts of wizardry which the officially-supported language is still trying to design; but you shouldn\u0026rsquo;t do it in your own code because there\u0026rsquo;s a very good chance that if it isn\u0026rsquo;t broken now, it will be broken soon. Technically ADL doesn\u0026rsquo;t step out of anywhere. It composes a list of the associated namespaces and searches them in one pass, while normal lookup runs from where the concept is defined, and the results of the two are merged. What the deleted function buys you is name hiding in that normal lookup: the presence of a particular name in an \u0026ldquo;inner\u0026rdquo; scope stops the lookup from reaching that name in \u0026ldquo;outer\u0026rdquo; scopes. But looking in one place and then stepping outwards echoes normal name lookup and is in many ways easier to understand.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://sub.fail/posts/opting-in/","summary":"The ways to manually opt your code into a function through the history of C++, and the black magic required to opt it back out again.","title":"Opting in, and back out again"},{"content":"I recently looked at P4313R1, a standards proposal paper which adds a set of bitmask operations for enums, using a C++26 annotation to opt-in. The core idea being that, to use an example from the paper, given code like the below:\nenum class [[=std::bitmask_type]] Permission { None = 0, Read = 1 \u0026lt;\u0026lt; 0, Write = 1 \u0026lt;\u0026lt; 1, Execute = 1 \u0026lt;\u0026lt; 2, }; The [[=std::bitmask_type]] annotation would automatically imbue Permission with an accessible set of bitwise operations so that you as a user needn\u0026rsquo;t write them out yourself. This got me thinking about the murky underbelly of C++ integer operations. Your C++ compiler will happily compute bitwise operations for any integral type as a builtin operation. This extends to types which we don\u0026rsquo;t traditionally think of as integers, such as wchar_t, the UTF character types char8_t through char32_t, and bool. But slightly more happens here than meets the eye. Because when you attempt to perform an operation like a | b, and the type of a and b is an integral type smaller than int, the language does not operate on the bit patterns of a and b directly. They undergo integral promotion - they are promoted up to int as an intermediate state, the bit patterns of these two ints are combined, and the result is returned to you, still as an int.1 Consider the below:\n//Two shorts constexpr short perm_A {1 \u0026lt;\u0026lt; 0}; constexpr short perm_B {1 \u0026lt;\u0026lt; 1}; //And the result type of running a bitwise operation on them is int static_assert(std::same_as\u0026lt;decltype(perm_A | perm_B), int\u0026gt;); Now, for the most part this is harmless - if you cast the above perm_A | perm_B back to short then the unnecessary bytes are truncated away and you\u0026rsquo;re left with a short which contains exactly the value it would have held if you\u0026rsquo;d combined perm_A and perm_B as short directly. And if you want to be certain that you are keeping your types consistent, and avoid the pernicious bugs of silent narrowing conversions, you can get into the habit of static_cast-ing the result of your bitwise operation to its original type. The important thing here however is that integral promotion is not optional. Unlike most other areas of C++ the developer doesn\u0026rsquo;t get a choice - your types will unavoidably be promoted for these operations.\nThe second part of what makes this a hazard for bool specifically is boolean conversion, a special case which does not truncate but instead explicitly converts zero to false and non-zero values to true. For these non-zero values, whatever bit pattern was previously stored is discarded and replaced with true, which has an integer value of 1. So if we take code like this:\nint x{10}; bool b{static_cast\u0026lt;bool\u0026gt;(x)}; and look at the generated asm (in this case from unoptimised x86-64 gcc 16.2):\nmov DWORD PTR [rbp-4], 10 ;Store the value of 10 cmp DWORD PTR [rbp-4], 0 ;Then compare to 0, set ZF if x is zero setne al ;Write 1 to AL if ZF is clear mov BYTE PTR [rbp-5], al ;Store the result The standard (specifically [conv.bool]) bases converting to bool on the only possible values which bool can hold - true and false, regardless of whatever bit pattern may have originally been used to create them.\nPutting it together With all of that covered, let\u0026rsquo;s talk through what happens when you try to evaluate ~true and then cast the result to a bool:\nThe value true is promoted to an int with a value of 1. The complement of 1 as an int is calculated as -2, as two\u0026rsquo;s complement behaviour is required as of C++20. The value of -2 is then cast back down to bool, undergoes boolean conversion, and since -2 is non-zero, becomes true. There you have it, the complement of true is true, or spelled in C++ static_cast\u0026lt;bool\u0026gt;(~true) == true.\nThis brings us back to enums. An enum is permitted to use any integral type as its underlying type, including our good friend bool. So let\u0026rsquo;s define one:\nenum class [[=std::bitmask_type]] boolean : bool{ FALSE, TRUE, }; Let\u0026rsquo;s also look at the bitwise complement operator as laid out in P4313R1:\ntemplate\u0026lt;bitmask-like T\u0026gt; constexpr T operator~ (T lhs) noexcept { return static_cast\u0026lt;T\u0026gt;(~to_underlying(lhs)); } By now the workings of that operator should be a familiar shape - first we convert the enum to its underlying type (in our case bool), then integral promotion is applied if that type is lower ranked than int, then we perform the bitwise operation, then we cast back to the enum type. As we would expect:\nstatic_assert(~boolean::TRUE == boolean::TRUE); Godbolt here.\nThis has all the potential to be a slightly confusing corner case in the language.\nWhen it\u0026rsquo;s false There is one exceptional case here which compounds the problem. If we try to run that same example in gcc, we get a different result:\nstatic_assert(~boolean::TRUE == boolean::FALSE); Godbolt here.\nSo what is going on with gcc? If we move the operation to runtime by defining two functions which depend on it as a runtime value:\nenum class boolean : bool{ FALSE, TRUE, }; void f(int x, bool\u0026amp; out) { out = static_cast\u0026lt;bool\u0026gt;(x); } void g(int x, boolean\u0026amp; out) { out = static_cast\u0026lt;boolean\u0026gt;(x); } and look at the asm (again unoptimised x86-64), then we get:\n\u0026#34;f(int, bool\u0026amp;)\u0026#34;: push rbp mov rbp, rsp mov DWORD PTR [rbp-4], edi mov QWORD PTR [rbp-16], rsi cmp DWORD PTR [rbp-4], 0 setne dl ;same setne pattern from [conv.bool] earlier mov rax, QWORD PTR [rbp-16] mov BYTE PTR [rax], dl nop pop rbp ret \u0026#34;g(int, boolean\u0026amp;)\u0026#34;: push rbp mov rbp, rsp mov DWORD PTR [rbp-4], edi mov QWORD PTR [rbp-16], rsi mov eax, DWORD PTR [rbp-4] and eax, 1 ;store the low bit in eax mov rdx, QWORD PTR [rbp-16] mov BYTE PTR [rdx], al ;and move al to the out-param nop pop rbp ret What we see is that when the type is not spelled bool, gcc will truncate down to the lowest bit rather than perform a boolean conversion. Any even value, when cast down, will produce boolean::FALSE, and any odd one will produce boolean::TRUE.\nThis is unique to enum types specifically, gcc generally performs consistently when handling bool directly:\nconstexpr boolean b{boolean::TRUE}; static_assert(static_cast\u0026lt;bool\u0026gt;(~b) == false); static_assert(static_cast\u0026lt;bool\u0026gt;(~true) == true); static_assert(std::to_underlying(~b) == false); static_assert(static_cast\u0026lt;bool\u0026gt;(~std::to_underlying(b)) == true); In contrast, Clang and MSVC will evaluate the result of all four of these complement-and-cast combinations as true, which is what the standard says they must be. Full godbolt comparison here. This seems to be a fairly long-lived conformance bug in gcc.\nUnscoped enums, promotion, and undefined behaviour But there is one place in the standard where integral promotion rules go further and introduce a new vector for undefined behaviour to enter into your program when doing these bitmask operations. Consider this code:\nenum nums{ zero, one, two, three, }; //This is implementation defined but holds on gcc and Clang. static_assert(std::is_same_v\u0026lt;std::underlying_type_t\u0026lt;nums\u0026gt;, unsigned int\u0026gt;); //nums uses an unsigned int as its base, therefore the entire domain of representable values is \u0026gt;= 0. //So let\u0026#39;s test this: static_assert(~one \u0026gt;= 0); //FAILS Godbolt here\nLooking at the diagnostic, gcc gives us:\n\u0026lt;source\u0026gt;:15:20: error: static assertion failed 15 | static_assert(~one \u0026gt;= 0); //FAILS | ~~~~~^~~~ • the comparison reduces to \u0026#39;(-2 \u0026gt;= 0)\u0026#39; So what happened here? Well, the passage in the standard which covers integral promotion, [conv.prom], carves out a special bullet for unscoped enumeration types with no fixed underlying type to behave differently from integer types when promoting. If the entire range of values can be stored in an int, then that is the type which it promotes to, then it tries unsigned int, and if that fails it repeats this signed-then-unsigned pattern for long and long long until it finds a suitable type. But, where this differs from plain integer types is that it only considers the effective range of representable values; and so an enum backed by unsigned int will promote to int regardless of the normal conversion ranking which would forbid it for integral types. As before, the fact that you are calling the builtin operator via ~one will unavoidably promote it to int, its complement is calculated as -2, and then the comparison is performed between two ints, and fails. If instead you convert to the underlying type first then you really see the asymmetry - static_assert(~std::to_underlying(one) \u0026gt;= 0); succeeds, and is so vacuously true that gcc even warns about its redundancy.\nBut this is only half the battle, and we need to talk about converting the result of our bitwise operation back to the enum type, and this is where UB creeps in. An unscoped enum with no specified underlying type defines itself in terms of a valid range of values determined by its enumerators independently of the range of whatever actual type is used by the compiler to back it. [dcl.enum] tells us the value range of such an enum is the value range of a hypothetical integer type of width M, where M is the minimum bits required to represent all enumerators. To demonstrate, consider a few examples:\nenum small{ //Range of enumerators is 0..1, so value range is that of an unsigned, one-bit integer. a = 0, b = 1, }; enum medium{ //Range of enumerators is 0..7, so value range is that of an unsigned, three-bit integer c = 0, d = 4, e = 7 }; enum large{ //Range of enumerators is -1..100, so value range is that of a signed, eight-bit integer f = -1, g = 10, h = 100, }; Even though your compiler will quite likely store these enums as unsigned int and int, only a subset of possible representable values is valid to use - that of the M-width integer. [expr.static.cast] in the standard makes it clear that it is undefined behaviour to cast a value outside of that range to the enum type. So, using the enumerators of the above example:\nstatic_cast\u0026lt;small\u0026gt;(1); //Well-defined static_cast\u0026lt;small\u0026gt;(2); //UB static_cast\u0026lt;medium\u0026gt;(3); //Well-defined: while not a value of an enumerator, it is within the range of an unsigned, three-bit integer static_cast\u0026lt;medium\u0026gt;(8); //UB static_cast\u0026lt;large\u0026gt;(-128); //Well-defined static_cast\u0026lt;large\u0026gt;(128); //UB This is our danger zone. The bitwise operators, in particular operator~, can produce values which are out of the well-defined value range for an enum, and user-defined operators which attempt to cast this value back down to the enum type can invoke UB.\nBut, I hear you cry, what about the example earlier with ~one? We saw that give a value of -2 which is outside the value range of nums. This is true, but here is where integral promotion actually saves us - one was promoted to int before we calculated its complement, and it was never cast back down again to invoke our UB. We only get into dangerous territory with a user-defined operator which converts the result of the operation back down to the original enum type.\nIt is also important to note that this UB risk only applies to unscoped \u0026ldquo;plain\u0026rdquo; enums with no fixed underlying type. All scoped enum types have a fixed underlying type, even if they don\u0026rsquo;t specify it (in that case, int); and all unscoped enum types which do specify a fixed underlying type inherit that type\u0026rsquo;s value range instead of inventing their own.\nAvoiding this in your own code Let\u0026rsquo;s say that, like P4313, you are adding bitmask operations to enums for your own code. Now that we have more weirdness in this area of C++, how would you go about making sure that your code is correct and unconfusing? Avoiding the bool case is simple - just constrain away that the underlying type can\u0026rsquo;t be bool, either for all enums or for the complement operator. But the standard doesn\u0026rsquo;t come with a handy type trait or reflection metafunction to specifically detect an unscoped enum with no fixed underlying type. Fortunately we can make one:\ntemplate\u0026lt;typename E\u0026gt; concept unfixed_enum = std::is_enum_v\u0026lt;E\u0026gt; \u0026amp;\u0026amp; !requires { E{0}; }; [dcl.init.list] only permits this initialization from a scalar for enums with a fixed underlying type, and 0 is the only possible value which is representable for all possible enums. To cover the edge cases, the standard defines an enum with no enumerators as having the value range of an unsigned, one-bit integer; and the thing which excludes 1 as a possible value is enum E{ a = -1 };, which has a range [-1, 0]. Be sure not to forget the std::is_enum_v. After all, there are many types for which T{0} is ill-formed, and most of them are not enums.\nThis post has mostly been focused on the bitwise complement operator, but the UB trap of an unfixed enum also applies to the bitwise left shift operator. If you take the route of constraining per operation, don\u0026rsquo;t let it slip your mind.\nThis brings us back once again to P4313R1. At time of writing, the concept which constrains these operators only constrains the type to be some enumeration type annotated with [[=std::bitmask_type]]. As such, it will allow confusing behaviour on enum types which are backed by bool, and potential UB on plain enums with no fixed underlying type. If the authors don\u0026rsquo;t want to standardise the above init-list trick, they could constrain it on std::is_scoped_enum as a conservative approach to prevent users from having easy access to the UB, since all unscoped enums can use the builtin bitwise operators anyway (albeit returning int). They could also constrain the underlying type to not be bool to minimise the confusion; particularly as the diagnostic which would normally get issued for complementing a bool in user code would be suppressed by default if it came from a system header. I do intend to contact the authors of P4313 about this to see if they want to add these constraints to their paper.\nAside: What about floating point? You might be wondering how the standard defines conversions of floating point numbers to these bool-backed enums. It is perhaps unsurprising - [expr.static.cast] requires that the behaviour is equivalent to first converting the floating point value to the underlying type of the enum, and then converting it to the enum type; pointing the user to [conv.fpint] which has an explicit note directing readers to our old friend [conv.bool]. Per the standard, this should mean that any floating point value other than exactly 0.0 or -0.0 would convert to boolean::TRUE, otherwise we end up back at boolean::FALSE.\nSo let\u0026rsquo;s start small:\nenum class boolean : bool { FALSE, TRUE, }; static_assert(static_cast\u0026lt;boolean\u0026gt;(0.0) == boolean::FALSE); static_assert(static_cast\u0026lt;boolean\u0026gt;(0.5) == boolean::TRUE); static_assert(static_cast\u0026lt;boolean\u0026gt;(2.5) == boolean::TRUE); Godbolt here.\nBoth Clang and gcc reject this code. The errors are that (boolean)2.5e+0 is not a constant expression; and static_cast\u0026lt;boolean\u0026gt;(0.5) == boolean::TRUE is wrong, and is instead equal to boolean::FALSE. MSVC accepts the above code.\nBut let\u0026rsquo;s go further and inspect what actually gets stored there. We write a function to examine the bit pattern generated from these casts, then try some values:\nvoid show(double d) { const boolean e = static_cast\u0026lt;boolean\u0026gt;(d); unsigned char byte {}; std::memcpy(\u0026amp;byte, \u0026amp;e, 1); std::println(\u0026#34;static_cast\u0026lt;boolean\u0026gt;({:7.1f}) stored byte {:3} required {}\u0026#34;, d, byte, (d != 0.0) ? 1 : 0); } int main() { show(0.0); show(0.5); show(1.0); show(2.5); show(3.0); show(256.0); show(-2.5); } Godbolt here.\nThe above code compiles without warning on all three compilers, but while MSVC again does the right thing in all cases, gcc and Clang\u0026rsquo;s behaviour is much more worrisome. Looking at the output, we see that gcc will always just truncate the value to an integer, then load that bit pattern into the resulting boolean. So 2.5 truncates to 2 and gives a boolean whose underlying bit pattern is 2. Clang does the same thing on unoptimised builds, but when optimisation is turned on, the truncated value then goes through the [conv.bool] transformation as normal and produces the right answer for all non-zero values outside of the range (-1, 1).\nThis is more concerning than some funky bit patterns, however. The valid range of boolean is [0, 1]. It is UB to read objects with bit patterns outside of this range. But, I hear you ask, what happens if we take these booleans and cast them back to bool, triggering a boolean conversion with no initial floating point state? MSVC again does the right thing; gcc doesn\u0026rsquo;t modify the bit patterns, leaving us with bool which are out of range of the type (and therefore UB to read); and Clang is where the fun happens again. Running the above code with a cast from e to a bool, and then memcpy-ing that bool into the unsigned char, we get this result for unoptimised builds:\n0.0 0.5 1.0 2.5 3.0 256.0 -2.5 required 0 1 1 1 1 1 1 gcc 14 / 15 / 16 0 0 1 2 3 0 254 clang 19 / 20 / 21 0 0 1 0 1 0 0 clang 23.1.1 0 0 1 1 1 0 1 Versions of Clang before 23 appear to simply perform trunc(d) \u0026amp; 1; meaning that after truncation, odd numbers are true and even numbers are false. Clang 23 performs (trunc(d) % 256) != 0, narrowing to a byte first. So 256.0 becomes FALSE and 257.0 becomes TRUE. Optimised builds act as before - truncating then doing the proper boolean conversion.\nUltimately this all comes to a rather absurd head. Consider the below code:\n#include \u0026lt;print\u0026gt; enum class boolean : bool { FALSE, TRUE, }; //Force this to be runtime boolean make(double d) { return static_cast\u0026lt;boolean\u0026gt;(d); } int main() { const boolean e = make(2.5); std::println(\u0026#34;e == boolean::TRUE : {}\u0026#34;, e == boolean::TRUE); std::println(\u0026#34;e == boolean::FALSE : {}\u0026#34;, e == boolean::FALSE); const bool b = static_cast\u0026lt;bool\u0026gt;(e); std::println(\u0026#34;b == true : {}\u0026#34;, b == true); std::println(\u0026#34;b == false : {}\u0026#34;, b == false); std::println(\u0026#34;b + 0 : {}\u0026#34;, b + 0); } Godbolt here.\nMSVC again leads the pack in giving the correct answer. Clang gets it exactly wrong and thinks that e is FALSE and b is false. gcc takes things up a notch, by providing an instance of an enum which compares equal to all of its enumerators, and a bool which is neither true nor false, until you turn on the optimiser and get a bool which is both true and false.\nAnd to hit all the obligatory targets of any conversation on floating point, infinity and NaN show as 0 and become FALSE as boolean and so cast to false as bool on gcc and Clang; and give 1 and therefore TRUE and true on MSVC.\nIn lighter news, Clang trunk seems to have fixed the above example to behave correctly, so some of the issues described in this section should be patched out soon.\nConclusion What did we learn from all this? Perhaps that even sensible and uncontentious pure-library-level papers such as P4313 should be on their guard against a footgun from some exotic C++ edge case. Or perhaps, in more practical terms:\nIntegral promotion is unavoidable. If you think you are operating on an integer which is smaller than int, there\u0026rsquo;s a good chance it became an int right under your nose. The bitwise complement of a bool is always true, even when wrapped in an enum. However you should not rely on this behaviour as gcc has a longstanding bug which gets this wrong (or right, if you prefer logic to C++). Unscoped enumeration types with no fixed underlying type have a range of valid values which may be smaller than that of whatever type actually underlies them. To be clear, the ranking order of integral promotion is more complex than just \u0026ldquo;convert to int\u0026rdquo;. For an integer type of lower conversion rank (not size) than int, if all values of that type can be represented as int then int is chosen. Otherwise unsigned int is chosen. In practice this will usually come out as int; but on implementations where int and short are the same size, unsigned short will promote directly to unsigned int. And for other types such as the charN_t family, conversion can proceed to longer integer types such as long and long long. bool is singled out as a special case and always promotes to int.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://sub.fail/posts/complement-of-true/","summary":"Consequences of integral promotion, UB on unscoped enums, and why boolean conversion is surprising.","title":"The complement of true is true, except when it's false"}]