[atomic] RFC: Bitwise operations for enums

Hi, There were quite a few times when I wanted to use bitwise operations on atomic<enum>, where the enum lists a number of flags that could be combined with bitwise AND, OR and XOR. Neither std::atomic nor Boost.Atomic enables bitwise operations for enums, so I had to resort to workarounds, such as using atomic<underlying_type_t<enum>> with explicit casts to and from the underlying type of the enum. Obviously, this is rather tedious, and I would prefer not to have to do this. I'm not sure what was the original rationale for not allowing bitwise operations for std::atomic<enum>, but I would guess these points were a part of it: 1. Not all enums represent bit masks, so for some enums the bitwise operations wouldn't make sense. 2. There may be user-defined bitwise operators defined for an enum, and presumably atomic<enum> would not invoke them to implement atomic bitwise operations. (Because it is generally not possible to make an arbitrary user's operator implementation atomic, other than to wrap it in a CAS loop, which defeats the point of having a dedicated atomic operation.) Some users may find this surprising. 3. For some operations, there may be multiple reasonable implementations. For example, bitwise complement (operator~) may be inverting all bits in the enum representation or only those bits, for which there are named values. An atomic<enum> could possibly implement the former, but the user's operator~ could implement the latter, which would introduce a discrepancy in behavior. Also, ironically, if atomic<enum> attempted to solve the problem #1 by detecting presence of user-defined bitwise operators for the enum and took it as a sign that the enum is a bit mask, this would only emphasize the problems #2 and #3. So, clearly, on one hand the functionality could be useful in practice, and on the other hand there may be pitfalls and confusion coming from it. I wonder what people think about this. Would you prefer to have the support and deal with the consequences or the status quo? Perhaps, you had your own solutions to this problem? Maybe there are other strong reasons to not have it? Personally, I'm leaning towards having the functionality rather than not. You can always not use what you don't want or need, but not the other way around. Thanks.

On Wed, Jun 11, 2025 at 1:34 AM Andrey Semashev via Boost < boost@lists.boost.org> wrote:
Hi,
There were quite a few times when I wanted to use bitwise operations on atomic<enum>, where the enum lists a number of flags that could be combined with bitwise AND, OR and XOR.
May I ask a bit more about motivation? More precisely why you prefer this over just using unsigned integers for flag? If you are worried about uniqueness of set bit it can be checked at compile time, e.g. something like this: https://godbolt.org/z/xqG866M8h (did not properly test this, so it may have bugs, but this is the idea). I like "scope" for flags(e,g, Color in Color::Red) but that can be done with namespaces, i.e. namespace Color{ constexpr uint32_t Red ... ,

On 11 Jun 2025 13:15, Ivan Matek wrote:
On Wed, Jun 11, 2025 at 1:34 AM Andrey Semashev via Boost <boost@lists.boost.org <mailto:boost@lists.boost.org>> wrote:
Hi,
There were quite a few times when I wanted to use bitwise operations on atomic<enum>, where the enum lists a number of flags that could be combined with bitwise AND, OR and XOR.
May I ask a bit more about motivation? More precisely why you prefer this over just using unsigned integers for flag? If you are worried about uniqueness of set bit it can be checked at compile time, e.g. something like this: https://godbolt.org/z/xqG866M8h <https://godbolt.org/z/xqG866M8h> (did not properly test this, so it may have bugs, but this is the idea).
I like "scope" for flags(e,g, Color inColor::Red) but that can be done with namespaces, i.e. namespace Color{ constexpr uint32_t Red ...,
If you have atomic<uint32_t> in your code then it isn't clear what this atomic value is about. It may be a counter, or an index, or a bit mask, or it may be something else entirely. If it is a bit mask, which bits are used and what meaning do they have. You can probably infer this information from the surrounding code and comments, but I would rather prefer atomic<enum>, where the enum part immediately gives you that information. It is enough to just follow to the enum definition to see which values are valid and whether it is a bit mask. It doesn't end with the atomic object declaration either. The surrounding code becomes more clear and type safe as well, as the values that you read and write into the atomic are properly typed with the enum. For example, you won't be putting a random wrong number into the atomic or you won't be passing the value from the atomic where it doesn't belong. The scope part is actually a secondary concern to me. Yes, it is useful to have the value names scoped, but it doesn't always matter all that much, if the enum values have names that are unique enough. The proper types of the constants and the atomic are much more important, IMO.

On Wed, Jun 11, 2025 at 4:05 PM Andrey Semashev via Boost < boost@lists.boost.org> wrote:
If you have atomic<uint32_t> in your code then it isn't clear what this atomic value is about. It may be a counter, or an index, or a bit mask, or it may be something else entirely. If it is a bit mask, which bits are used and what meaning do they have. You can probably infer this information from the surrounding code and comments, but I would rather prefer atomic<enum>, where the enum part immediately gives you that information. It is enough to just follow to the enum definition to see which values are valid and whether it is a bit mask.
I agree with your concern about readability, but would strong typedef work here? I am not an atomic expert, but it seems wrapping uint32_t in a simple struct does not destroy lockfree property of atomic. https://godbolt.org/z/WfGx76vxo

On 11 Jun 2025 17:47, Ivan Matek wrote:
On Wed, Jun 11, 2025 at 4:05 PM Andrey Semashev via Boost <boost@lists.boost.org <mailto:boost@lists.boost.org>> wrote:
If you have atomic<uint32_t> in your code then it isn't clear what this atomic value is about. It may be a counter, or an index, or a bit mask, or it may be something else entirely. If it is a bit mask, which bits are used and what meaning do they have. You can probably infer this information from the surrounding code and comments, but I would rather prefer atomic<enum>, where the enum part immediately gives you that information. It is enough to just follow to the enum definition to see which values are valid and whether it is a bit mask.
I agree with your concern about readability, but would strong typedef work here? I am not an atomic expert, but it seems wrapping uint32_t in a simple struct does not destroy lockfree property of atomic.
https://godbolt.org/z/WfGx76vxo <https://godbolt.org/z/WfGx76vxo>
It would work, but it also would not enable bitwise operations on the atomic. And regarding integers, we already have a strong typedef in the language, it is called enums. :)

Am 11.06.25 um 17:20 schrieb Andrey Semashev via Boost:
On 11 Jun 2025 17:47, Ivan Matek wrote:
I agree with your concern about readability, but would strong typedef work here? I am not an atomic expert, but it seems wrapping uint32_t in a simple struct does not destroy lockfree property of atomic.
https://godbolt.org/z/WfGx76vxo <https://godbolt.org/z/WfGx76vxo> It would work, but it also would not enable bitwise operations on the atomic. And regarding integers, we already have a strong typedef in the language, it is called enums. :)
I fully agree with Andrey here: https://godbolt.org/z/4c5E53czz Compares the 2 approaches and I very much like the enum approach which does not pollute the global scope with constants that should be scoped too but isn't possibly with the strong type. See:
auto configuration = kUseSimd | kUsePrefetching;
auto config = ConfigEnum::UseSimd | ConfigEnum::UsePrefetching;
I'm using the 2nd approach in a couple of projects, usually with a macro or trait to enable the bitwise operators. As for atomic enums: I wouldn't expect too much there as e.g. `config |= ConfigEnum::UseSimd` wouldn't be reliable for atomics without a compare&swap. You noted that already for "user-defined bitwise operators defined for an enum,", so I guess you are aware of that already.

On Thu, Jun 12, 2025 at 10:24 AM Alexander Grund via Boost < boost@lists.boost.org> wrote:
Compares the 2 approaches and I very much like the enum approach which does not pollute the global scope with constants that should be scoped too but isn't possibly with the strong type. See:
auto configuration = kUseSimd | kUsePrefetching;
auto config = ConfigEnum::UseSimd | ConfigEnum::UsePrefetching;
As I said before you can limit the scope by introducting a namespace, i.e. namespace ConfigEnum { All this is stylistic obviously, but I do not like that you are creating enum combination that does not represent any value from enum set. Now we both know in C++ everything is allowed :) and then some more, so you can freely bitwise or enums... BTW Interesting proposal from 9 years ago: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0375r0.html I am mostly talking about conceptual meaning of enum: set<int> is not int. Again to be clear I am not arguing with you, just saying why I prefer one style. And to keep our toy example from exploding into a full prototype I will just briefly recap what would I prefer to see in "full" solution 1. difference between flag, and flag combination 2. wrap atomic operations so users can operate on ConfigFlag, and ConfigFlagCombination(or AtomicConfigFlagCombination if we feel using atomic for each use case is too slow), while underlying logic is as if they are operating on atomic<uint32_t> 1. is : struct ConfigFlag { uint32_t val; }; // combination of 0 or more ConfigFlag struct ConfigFlagCombination { uint32_t val; }; ConfigFlagSet operator | (ConfigFlag a, ConfigFlag b) { return {a.val|b.val}; } 2. is: TODO :) I believe this may be considered over engineering a simple problem, but in large codebase where this pattern is used a lot might be worth it.

On 12 Jun 2025 11:53, Ivan Matek via Boost wrote:
On Thu, Jun 12, 2025 at 10:24 AM Alexander Grund via Boost < boost@lists.boost.org> wrote:
Compares the 2 approaches and I very much like the enum approach which does not pollute the global scope with constants that should be scoped too but isn't possibly with the strong type. See:
auto configuration = kUseSimd | kUsePrefetching;
auto config = ConfigEnum::UseSimd | ConfigEnum::UsePrefetching;
As I said before you can limit the scope by introducting a namespace, i.e. namespace ConfigEnum {
The namespace cannot have the same name as the wrapper class, and you can't define named values as static members of the class. https://godbolt.org/z/xhbE8M77r So you can't achieve the same syntax benefits as with enums, where the enum values are syntactically scoped within the enum type. And beside that, the amount of boilerplate that is required by the struct approach is considerably greater than the enum, with no clear benefit in the end.
I am mostly talking about conceptual meaning of enum: set<int> is not int.
The standard defines enumerations as "a distinct type with named constants". That is, its a way to define a number of named constants and associate them with a type. The standard then goes on to say that an enumeration has an underlying type. It doesn't say that the set of values of the enum type is limited to the named constants (as it says e.g. for bool); other values, as long as those values are representable by the underlying type, are implicitly allowed. So when you're saying an enum cannot have values other than the named constants (I'm assuming that's what you mean by "set<int>" above), you are talking about one particular usage scenario of the enum, where you simply don't use values other than the named constants. That's not the only valid usage scenario, and enums implementing bit masks are not a misuse of enums.

On Thu, Jun 12, 2025 at 1:37 PM Andrey Semashev via Boost < boost@lists.boost.org> wrote:
I am mostly talking about conceptual meaning of enum: set<int> is not int.
The standard defines enumerations as "a distinct type with named constants". That is, its a way to define a number of named constants and associate them with a type. The standard then goes on to say that an enumeration has an underlying type. It doesn't say that the set of values of the enum type is limited to the named constants (as it says e.g. for bool); other values, as long as those values are representable by the underlying type, are implicitly allowed. So when you're saying an enum cannot have values other than the named constants (I'm assuming that's what you mean by "set<int>" above), you are talking about one particular usage scenario of the enum, where you simply don't use values other than the named constants. That's not the only valid usage scenario, and enums implementing bit masks are not a misuse of enums.
Yeah when I sent an email I realized I should have been more precise. I
know what current state of language is, I just think it is bad, and I try to avoid confusing/bad parts of C++. Again according to me, I am not trying to convert you, I understand this is personal preference. Sankel proposal I linked explains in detail what I am talking about: In C++ enum is a "union" of different things(not to be confused with union as language construct). I dislike that since it makes reasoning harder, i.e. it would be better if that functionality was split. So in my ideal world flag is a language feature and enum is a language feature, so | is not allowed on enum values. Sankel proposal wanted to add [[exhaustive]] that beside ugly syntax is something I would like to see in the language.

Andrey Semashev wrote:
The standard defines enumerations as "a distinct type with named constants". That is, its a way to define a number of named constants and associate them with a type. The standard then goes on to say that an enumeration has an underlying type. It doesn't say that the set of values of the enum type is limited to the named constants (as it says e.g. for bool); other values, as long as those values are representable by the underlying type, are implicitly allowed.
Not quite. For scoped enums, and those with fixed underlying type, all values of the underlying type is allowed. For "normal" enums, it's a bit more complex; the minimum number of bits that can represent all enumerators is computed, and values that fit into this number of bits are allowed. So any bitwise combination of enumerators is allowed, but negation may not be.

On 12 Jun 2025 18:56, Peter Dimov via Boost wrote:
Andrey Semashev wrote:
The standard defines enumerations as "a distinct type with named constants". That is, its a way to define a number of named constants and associate them with a type. The standard then goes on to say that an enumeration has an underlying type. It doesn't say that the set of values of the enum type is limited to the named constants (as it says e.g. for bool); other values, as long as those values are representable by the underlying type, are implicitly allowed.
Not quite. For scoped enums, and those with fixed underlying type, all values of the underlying type is allowed. For "normal" enums, it's a bit more complex; the minimum number of bits that can represent all enumerators is computed, and values that fit into this number of bits are allowed.
So any bitwise combination of enumerators is allowed, but negation may not be.
Yes, thank you for the correction.

On 12 Jun 2025 11:24, Alexander Grund via Boost wrote:
As for atomic enums: I wouldn't expect too much there as e.g. `config |= ConfigEnum::UseSimd` wouldn't be reliable for atomics without a compare&swap.
There are atomic instructions that implement bitwise operations directly, without a CAS loop. Including on x86, although not in all cases.
You noted that already for "user-defined bitwise operators defined for an enum,", so I guess you are aware of that already.
To be clear, I'm not suggesting that e.g. atomic<enum>::fetch_or() should be calling the user-defined operator| for the enum. *That* would require a CAS loop. No, I'm suggesting that atomic bitwise operations should be defined in terms of value representations, pretty much how compare_exchange_(weak|strong) is defined, which, as you know, compares values "as if with memcmp" and does not invoke any user-defined operator==().

On 11 Jun 2025 02:34, Andrey Semashev wrote:
Hi,
There were quite a few times when I wanted to use bitwise operations on atomic<enum>, where the enum lists a number of flags that could be combined with bitwise AND, OR and XOR. Neither std::atomic nor Boost.Atomic enables bitwise operations for enums, so I had to resort to workarounds, such as using atomic<underlying_type_t<enum>> with explicit casts to and from the underlying type of the enum. Obviously, this is rather tedious, and I would prefer not to have to do this.
I have now implemented this in Boost.Atomic. I will be shipped in the next Boost release.
participants (4)
-
Alexander Grund
-
Andrey Semashev
-
Ivan Matek
-
Peter Dimov