How do I write a maintainable, fast, compile-time bit-mask in C++?
I have some code that is more or less like this:
#include <bitset>
enum Flags { A = 1, B = 2, C = 3, D = 5,
E = 8, F = 13, G = 21, H,
I, J, K, L, M, N, O };
void apply_known_mask(std::bitset<64> &bits) {
const Flags important_bits = { B, D, E, H, K, M, L, O };
std::remove_reference<decltype(bits)>::type mask{};
for (const auto& bit : important_bits) {
mask.set(bit);
}
bits &= mask;
}
Clang >= 3.6 does the smart thing and compiles this to a single and
instruction (which then gets inlined everywhere else):
apply_known_mask(std::bitset<64ul>&): # @apply_known_mask(std::bitset<64ul>&)
and qword ptr [rdi], 775946532
ret
But every version of GCC I've tried compiles this to an enormous mess that includes error handling that should be statically DCE'd. In other code, it will even place the important_bits
equivalent as data in line with the code!
.LC0:
.string "bitset::set"
.LC1:
.string "%s: __position (which is %zu) >= _Nb (which is %zu)"
apply_known_mask(std::bitset<64ul>&):
sub rsp, 40
xor esi, esi
mov ecx, 2
movabs rax, 21474836482
mov QWORD PTR [rsp], rax
mov r8d, 1
movabs rax, 94489280520
mov QWORD PTR [rsp+8], rax
movabs rax, 115964117017
mov QWORD PTR [rsp+16], rax
movabs rax, 124554051610
mov QWORD PTR [rsp+24], rax
mov rax, rsp
jmp .L2
.L3:
mov edx, DWORD PTR [rax]
mov rcx, rdx
cmp edx, 63
ja .L7
.L2:
mov rdx, r8
add rax, 4
sal rdx, cl
lea rcx, [rsp+32]
or rsi, rdx
cmp rax, rcx
jne .L3
and QWORD PTR [rdi], rsi
add rsp, 40
ret
.L7:
mov ecx, 64
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:.LC1
xor eax, eax
call std::__throw_out_of_range_fmt(char const*, ...)
How should I write this code so that both compilers can do the right thing? Failing that, how should I write this so that it remains clear, fast, and maintainable?
c++ c++11 bit-manipulation
add a comment |
I have some code that is more or less like this:
#include <bitset>
enum Flags { A = 1, B = 2, C = 3, D = 5,
E = 8, F = 13, G = 21, H,
I, J, K, L, M, N, O };
void apply_known_mask(std::bitset<64> &bits) {
const Flags important_bits = { B, D, E, H, K, M, L, O };
std::remove_reference<decltype(bits)>::type mask{};
for (const auto& bit : important_bits) {
mask.set(bit);
}
bits &= mask;
}
Clang >= 3.6 does the smart thing and compiles this to a single and
instruction (which then gets inlined everywhere else):
apply_known_mask(std::bitset<64ul>&): # @apply_known_mask(std::bitset<64ul>&)
and qword ptr [rdi], 775946532
ret
But every version of GCC I've tried compiles this to an enormous mess that includes error handling that should be statically DCE'd. In other code, it will even place the important_bits
equivalent as data in line with the code!
.LC0:
.string "bitset::set"
.LC1:
.string "%s: __position (which is %zu) >= _Nb (which is %zu)"
apply_known_mask(std::bitset<64ul>&):
sub rsp, 40
xor esi, esi
mov ecx, 2
movabs rax, 21474836482
mov QWORD PTR [rsp], rax
mov r8d, 1
movabs rax, 94489280520
mov QWORD PTR [rsp+8], rax
movabs rax, 115964117017
mov QWORD PTR [rsp+16], rax
movabs rax, 124554051610
mov QWORD PTR [rsp+24], rax
mov rax, rsp
jmp .L2
.L3:
mov edx, DWORD PTR [rax]
mov rcx, rdx
cmp edx, 63
ja .L7
.L2:
mov rdx, r8
add rax, 4
sal rdx, cl
lea rcx, [rsp+32]
or rsi, rdx
cmp rax, rcx
jne .L3
and QWORD PTR [rdi], rsi
add rsp, 40
ret
.L7:
mov ecx, 64
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:.LC1
xor eax, eax
call std::__throw_out_of_range_fmt(char const*, ...)
How should I write this code so that both compilers can do the right thing? Failing that, how should I write this so that it remains clear, fast, and maintainable?
c++ c++11 bit-manipulation
4
Rather than using a loop, can't you construct a mask withB | D | E | ... | O
?
– HolyBlackCat
Feb 20 at 12:25
4
The enum has bit positions rather than already expanded bits, so I could do(1ULL << B) | ... | (1ULL << O)
– Alex Reinking
Feb 20 at 12:26
3
The downside being that the actual names are long and irregular and it's not nearly as easy to see which flags are in the mask with all that line noise.
– Alex Reinking
Feb 20 at 12:32
2
@AlexReinking You could make it one(1ULL << Constant)
| per line, and align the constant names on the different lines, that would be easier on the eyes.
– einpoklum
Feb 21 at 15:37
add a comment |
I have some code that is more or less like this:
#include <bitset>
enum Flags { A = 1, B = 2, C = 3, D = 5,
E = 8, F = 13, G = 21, H,
I, J, K, L, M, N, O };
void apply_known_mask(std::bitset<64> &bits) {
const Flags important_bits = { B, D, E, H, K, M, L, O };
std::remove_reference<decltype(bits)>::type mask{};
for (const auto& bit : important_bits) {
mask.set(bit);
}
bits &= mask;
}
Clang >= 3.6 does the smart thing and compiles this to a single and
instruction (which then gets inlined everywhere else):
apply_known_mask(std::bitset<64ul>&): # @apply_known_mask(std::bitset<64ul>&)
and qword ptr [rdi], 775946532
ret
But every version of GCC I've tried compiles this to an enormous mess that includes error handling that should be statically DCE'd. In other code, it will even place the important_bits
equivalent as data in line with the code!
.LC0:
.string "bitset::set"
.LC1:
.string "%s: __position (which is %zu) >= _Nb (which is %zu)"
apply_known_mask(std::bitset<64ul>&):
sub rsp, 40
xor esi, esi
mov ecx, 2
movabs rax, 21474836482
mov QWORD PTR [rsp], rax
mov r8d, 1
movabs rax, 94489280520
mov QWORD PTR [rsp+8], rax
movabs rax, 115964117017
mov QWORD PTR [rsp+16], rax
movabs rax, 124554051610
mov QWORD PTR [rsp+24], rax
mov rax, rsp
jmp .L2
.L3:
mov edx, DWORD PTR [rax]
mov rcx, rdx
cmp edx, 63
ja .L7
.L2:
mov rdx, r8
add rax, 4
sal rdx, cl
lea rcx, [rsp+32]
or rsi, rdx
cmp rax, rcx
jne .L3
and QWORD PTR [rdi], rsi
add rsp, 40
ret
.L7:
mov ecx, 64
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:.LC1
xor eax, eax
call std::__throw_out_of_range_fmt(char const*, ...)
How should I write this code so that both compilers can do the right thing? Failing that, how should I write this so that it remains clear, fast, and maintainable?
c++ c++11 bit-manipulation
I have some code that is more or less like this:
#include <bitset>
enum Flags { A = 1, B = 2, C = 3, D = 5,
E = 8, F = 13, G = 21, H,
I, J, K, L, M, N, O };
void apply_known_mask(std::bitset<64> &bits) {
const Flags important_bits = { B, D, E, H, K, M, L, O };
std::remove_reference<decltype(bits)>::type mask{};
for (const auto& bit : important_bits) {
mask.set(bit);
}
bits &= mask;
}
Clang >= 3.6 does the smart thing and compiles this to a single and
instruction (which then gets inlined everywhere else):
apply_known_mask(std::bitset<64ul>&): # @apply_known_mask(std::bitset<64ul>&)
and qword ptr [rdi], 775946532
ret
But every version of GCC I've tried compiles this to an enormous mess that includes error handling that should be statically DCE'd. In other code, it will even place the important_bits
equivalent as data in line with the code!
.LC0:
.string "bitset::set"
.LC1:
.string "%s: __position (which is %zu) >= _Nb (which is %zu)"
apply_known_mask(std::bitset<64ul>&):
sub rsp, 40
xor esi, esi
mov ecx, 2
movabs rax, 21474836482
mov QWORD PTR [rsp], rax
mov r8d, 1
movabs rax, 94489280520
mov QWORD PTR [rsp+8], rax
movabs rax, 115964117017
mov QWORD PTR [rsp+16], rax
movabs rax, 124554051610
mov QWORD PTR [rsp+24], rax
mov rax, rsp
jmp .L2
.L3:
mov edx, DWORD PTR [rax]
mov rcx, rdx
cmp edx, 63
ja .L7
.L2:
mov rdx, r8
add rax, 4
sal rdx, cl
lea rcx, [rsp+32]
or rsi, rdx
cmp rax, rcx
jne .L3
and QWORD PTR [rdi], rsi
add rsp, 40
ret
.L7:
mov ecx, 64
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:.LC1
xor eax, eax
call std::__throw_out_of_range_fmt(char const*, ...)
How should I write this code so that both compilers can do the right thing? Failing that, how should I write this so that it remains clear, fast, and maintainable?
c++ c++11 bit-manipulation
c++ c++11 bit-manipulation
edited Feb 20 at 20:42
grooveplex
1,10911323
1,10911323
asked Feb 20 at 12:20
Alex ReinkingAlex Reinking
3,0861834
3,0861834
4
Rather than using a loop, can't you construct a mask withB | D | E | ... | O
?
– HolyBlackCat
Feb 20 at 12:25
4
The enum has bit positions rather than already expanded bits, so I could do(1ULL << B) | ... | (1ULL << O)
– Alex Reinking
Feb 20 at 12:26
3
The downside being that the actual names are long and irregular and it's not nearly as easy to see which flags are in the mask with all that line noise.
– Alex Reinking
Feb 20 at 12:32
2
@AlexReinking You could make it one(1ULL << Constant)
| per line, and align the constant names on the different lines, that would be easier on the eyes.
– einpoklum
Feb 21 at 15:37
add a comment |
4
Rather than using a loop, can't you construct a mask withB | D | E | ... | O
?
– HolyBlackCat
Feb 20 at 12:25
4
The enum has bit positions rather than already expanded bits, so I could do(1ULL << B) | ... | (1ULL << O)
– Alex Reinking
Feb 20 at 12:26
3
The downside being that the actual names are long and irregular and it's not nearly as easy to see which flags are in the mask with all that line noise.
– Alex Reinking
Feb 20 at 12:32
2
@AlexReinking You could make it one(1ULL << Constant)
| per line, and align the constant names on the different lines, that would be easier on the eyes.
– einpoklum
Feb 21 at 15:37
4
4
Rather than using a loop, can't you construct a mask with
B | D | E | ... | O
?– HolyBlackCat
Feb 20 at 12:25
Rather than using a loop, can't you construct a mask with
B | D | E | ... | O
?– HolyBlackCat
Feb 20 at 12:25
4
4
The enum has bit positions rather than already expanded bits, so I could do
(1ULL << B) | ... | (1ULL << O)
– Alex Reinking
Feb 20 at 12:26
The enum has bit positions rather than already expanded bits, so I could do
(1ULL << B) | ... | (1ULL << O)
– Alex Reinking
Feb 20 at 12:26
3
3
The downside being that the actual names are long and irregular and it's not nearly as easy to see which flags are in the mask with all that line noise.
– Alex Reinking
Feb 20 at 12:32
The downside being that the actual names are long and irregular and it's not nearly as easy to see which flags are in the mask with all that line noise.
– Alex Reinking
Feb 20 at 12:32
2
2
@AlexReinking You could make it one
(1ULL << Constant)
| per line, and align the constant names on the different lines, that would be easier on the eyes.– einpoklum
Feb 21 at 15:37
@AlexReinking You could make it one
(1ULL << Constant)
| per line, and align the constant names on the different lines, that would be easier on the eyes.– einpoklum
Feb 21 at 15:37
add a comment |
6 Answers
6
active
oldest
votes
Best version is c++17:
template< unsigned char... indexes >
constexpr unsigned long long mask(){
return ((1ull<<indexes)|...|0ull);
}
Then
void apply_known_mask(std::bitset<64> &bits) {
constexpr auto m = mask<B,D,E,H,K,M,L,O>();
bits &= m;
}
back in c++14, we can do this strange trick:
template< unsigned char... indexes >
constexpr unsigned long long mask(){
auto r = 0ull;
using discard_t = int; // data never used
// value never used:
discard_t discard = {0,(void(
r |= (1ull << indexes) // side effect, used
),0)...};
(void)discard; // block unused var warnings
return r;
}
or, if we are stuck with c++11, we can solve it recursively:
constexpr unsigned long long mask(){
return 0;
}
template<class...Tail>
constexpr unsigned long long mask(unsigned char b0, Tail...tail){
return (1ull<<b0) | mask(tail...);
}
template< unsigned char... indexes >
constexpr unsigned long long mask(){
return mask(indexes...);
}
Godbolt with all 3 -- you can switch CPP_VERSION define, and get identical assembly.
In practice I'd use the most modern I could. 14 beats 11 because we don't have recursion and hence O(n^2) symbol length (which can explode compile time and compiler memory usage); 17 beats 14 because the compiler doesn't have to dead-code-eliminate that array, and that array trick is just ugly.
Of these 14 is the most confusing. Here we create an anonymous array of all 0s, meanwhile as a side effect construct our result, then discard the array. It has a number of 0s in it equal to the size of our pack, plus 1 (which we add so we can handle empty packs).
34
Sorry, but that C++14 code is something. I don't know what.
– James
Feb 20 at 18:34
14
@James It is a wonderful motivating example of why fold expressions in C++17 are very welcome. It, and similar tricks, turn out to be an efficient way to expand a pack "inplace" without any recursion and that compliers find easy to optimize.
– Yakk - Adam Nevraumont
Feb 20 at 19:10
3
@ruben multi line constexpr is illegal in 11
– Yakk - Adam Nevraumont
Feb 20 at 22:40
6
I can't see myself checking in that C++14 code. I'll stick to the C++11 one since I need that, anyway, but even if I could use it, the C++14 code requires so much explanation I wouldn't. These masks can always be written to have at most 32 elements, so I'm not worried about O(n^2) behavior. After all, if n is bounded by a constant, then it's really O(1). ;)
– Alex Reinking
Feb 21 at 7:30
8
To those trying to understand((1ull<<indexes)|...|0ull)
it is a "fold expression". Specifically it is a "binary right fold" and It should be parsed as(pack
op
...
op
init)
– Henrik Hansen
Feb 21 at 10:53
|
show 10 more comments
The optimization you're looking for seems to be loop peeling, which is enabled at -O3
, or manually with -fpeel-loops
. I'm not sure why this falls under the purview of loop peeling rather than loop unrolling, but possibly it's unwilling to unroll a loop with nonlocal control flow inside it (as there is, potentially, from the range check).
By default, though, GCC stops short of being able to peel all the iterations, which apparently is necessary. Experimentally, passing -O2 -fpeel-loops --param max-peeled-insns=200
(the default value is 100) gets the job done with your original code: https://godbolt.org/z/NNWrga
You are amazing thank you! I had no idea this was configurable in GCC! Though for some reason-O3 -fpeel-loops --param max-peeled-insns=200
fails... It's due to-ftree-slp-vectorize
apparently.
– Alex Reinking
Feb 20 at 19:41
This solution seems to be limited to the x86-64 target. The output for ARM and ARM64 is still not pretty, which then again might be completely irrelevant for OP.
– realtime
Feb 21 at 9:24
@realtime - it is somewhat relevant, actually. Thanks for pointing out that it doesn't work in this case. Very disappointing that GCC doesn't catch it before being lowered to a platform-specific IR. LLVM optimizes it before any further lowering.
– Alex Reinking
Feb 21 at 11:37
add a comment |
if using only C++11 is a must (&a)[N]
is a way to capture arrays. This allows you to write one single recursive function without using helper functions whatsoever:
template <std::size_t N>
constexpr std::uint64_t generate_mask(Flags const (&a)[N], std::size_t i = 0u){
return i < N ? (1ull << a[i] | generate_mask(a, i + 1u)) : 0ull;
}
assigning it to a constexpr auto
:
void apply_known_mask(std::bitset<64>& bits) {
constexpr const Flags important_bits = { B, D, E, H, K, M, L, O };
constexpr auto m = generate_mask(important_bits); //< here
bits &= m;
}
Test
int main() {
std::bitset<64> b;
b.flip();
apply_known_mask(b);
std::cout << b.to_string() << 'n';
}
Output
0000000000000000000000000000000000101110010000000000000100100100
// ^ ^^^ ^ ^ ^ ^
// O MLK H E D B
one really has to appreciate C++`s ability to calculate anything turing computable at compile time. It surely still blows my mind (<>).
For the later versions C++14 and C++17 yakk's answer already wonderfully covers that.
3
How does this demonstrate thatapply_known_mask
actually optimizes?
– Alex Reinking
Feb 20 at 21:50
2
@AlexReinking: All the scary bits areconstexpr
. And while that's theoretically not sufficient, we know that GCC is quite capable of evaluatingconstexpr
as intended.
– MSalters
Feb 21 at 12:37
add a comment |
I would encourage you to write a proper EnumSet
type.
Writing a basic EnumSet<E>
in C++14 (onwards) based on std::uint64_t
is trivial:
template <typename E>
class EnumSet {
public:
constexpr EnumSet() = default;
constexpr EnumSet(std::initializer_list<E> values) {
for (auto e : values) {
set(e);
}
}
constexpr bool has(E e) const { return mData & mask(e); }
constexpr EnumSet& set(E e) { mData |= mask(e); return *this; }
constexpr EnumSet& unset(E e) { mData &= ~mask(e); return *this; }
constexpr EnumSet& operator&=(const EnumSet& other) {
mData &= other.mData;
return *this;
}
constexpr EnumSet& operator|=(const EnumSet& other) {
mData |= other.mData;
return *this;
}
private:
static constexpr std::uint64_t mask(E e) {
return std::uint64_t(1) << e;
}
std::uint64_t mData = 0;
};
This allows you to write simple code:
void apply_known_mask(EnumSet<Flags>& flags) {
static constexpr EnumSet<Flags> IMPORTANT{ B, D, E, H, K, M, L, O };
flags &= IMPORTANT;
}
In C++11, it requires some convolutions, but remains possible nonetheless:
template <typename E>
class EnumSet {
public:
template <E... Values>
static constexpr EnumSet make() {
return EnumSet(make_impl(Values...));
}
constexpr EnumSet() = default;
constexpr bool has(E e) const { return mData & mask(e); }
void set(E e) { mData |= mask(e); }
void unset(E e) { mData &= ~mask(e); }
EnumSet& operator&=(const EnumSet& other) {
mData &= other.mData;
return *this;
}
EnumSet& operator|=(const EnumSet& other) {
mData |= other.mData;
return *this;
}
private:
static constexpr std::uint64_t mask(E e) {
return std::uint64_t(1) << e;
}
static constexpr std::uint64_t make_impl() { return 0; }
template <typename... Tail>
static constexpr std::uint64_t make_impl(E head, Tail... tail) {
return mask(head) | make_impl(tail...);
}
explicit constexpr EnumSet(std::uint64_t data): mData(data) {}
std::uint64_t mData = 0;
};
And is invoked with:
void apply_known_mask(EnumSet<Flags>& flags) {
static constexpr EnumSet<Flags> IMPORTANT =
EnumSet<Flags>::make<B, D, E, H, K, M, L, O>();
flags &= IMPORTANT;
}
Even GCC trivially generates an and
instruction at -O1
godbolt:
apply_known_mask(EnumSet<Flags>&):
and QWORD PTR [rdi], 775946532
ret
1
In c++11 much of yourconstexpr
code isn't legal. I mean, some has 2 statements! (C++11 constexpr sucked)
– Yakk - Adam Nevraumont
Feb 20 at 17:12
@Yakk-AdamNevraumont: You did realize that I posted 2 versions of the code, the first for C++14 onward, and a second specially tailored for C++11? (to account for its limitations)
– Matthieu M.
Feb 20 at 17:29
1
It might be better to use std::underlying_type instead of std::uint64_t.
– James
Feb 20 at 18:32
@James: Actually, no. Do note thatEnumSet<E>
does not use a value ofE
as value directly, but instead uses1 << e
. It's a different domain altogether, which is actually what makes the class so valuable => no chance of accidentally indexing bye
instead of1 << e
.
– Matthieu M.
Feb 20 at 18:53
@MatthieuM. Yes you're right. I'm confusing it with our own implementation which is very similar to yours. The disadvantage of using (1 << e) is that if e is out of bounds for the underlying_type's size then it's probably UB, hopefully a compiler error.
– James
Feb 20 at 18:55
|
show 4 more comments
Since C++11 you could also use classic TMP technique:
template<std::uint64_t Flag, std::uint64_t... Flags>
struct bitmask
{
static constexpr std::uint64_t mask =
bitmask<Flag>::value | bitmask<Flags...>::value;
};
template<std::uint64_t Flag>
struct bitmask<Flag>
{
static constexpr std::uint64_t value = (uint64_t)1 << Flag;
};
void apply_known_mask(std::bitset<64> &bits)
{
constexpr auto mask = bitmask<B, D, E, H, K, M, L, O>::value;
bits &= mask;
}
Link to Compiler Explorer: https://godbolt.org/z/Gk6KX1
The advantage of this approach over template constexpr function is that it's potentially slightly faster to compile due to rule of Chiel.
add a comment |
There are some far to 'clever' ideas here. You are probably not helping maintainability by following them.
is
{B, D, E, H, K, M, L, O};
so much easier to write than
(B| D| E| H| K| M| L| O);
?
Then none of the rest of the code is needed.
1
"B", "D", etc are not flags themselves.
– Michał Łoś
Feb 22 at 12:01
Yes, you would need to transform these to flags first. That's not at all clear in my answer. sorry. I will update.
– ANone
Feb 22 at 12:08
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f54786278%2fhow-do-i-write-a-maintainable-fast-compile-time-bit-mask-in-c%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
6 Answers
6
active
oldest
votes
6 Answers
6
active
oldest
votes
active
oldest
votes
active
oldest
votes
Best version is c++17:
template< unsigned char... indexes >
constexpr unsigned long long mask(){
return ((1ull<<indexes)|...|0ull);
}
Then
void apply_known_mask(std::bitset<64> &bits) {
constexpr auto m = mask<B,D,E,H,K,M,L,O>();
bits &= m;
}
back in c++14, we can do this strange trick:
template< unsigned char... indexes >
constexpr unsigned long long mask(){
auto r = 0ull;
using discard_t = int; // data never used
// value never used:
discard_t discard = {0,(void(
r |= (1ull << indexes) // side effect, used
),0)...};
(void)discard; // block unused var warnings
return r;
}
or, if we are stuck with c++11, we can solve it recursively:
constexpr unsigned long long mask(){
return 0;
}
template<class...Tail>
constexpr unsigned long long mask(unsigned char b0, Tail...tail){
return (1ull<<b0) | mask(tail...);
}
template< unsigned char... indexes >
constexpr unsigned long long mask(){
return mask(indexes...);
}
Godbolt with all 3 -- you can switch CPP_VERSION define, and get identical assembly.
In practice I'd use the most modern I could. 14 beats 11 because we don't have recursion and hence O(n^2) symbol length (which can explode compile time and compiler memory usage); 17 beats 14 because the compiler doesn't have to dead-code-eliminate that array, and that array trick is just ugly.
Of these 14 is the most confusing. Here we create an anonymous array of all 0s, meanwhile as a side effect construct our result, then discard the array. It has a number of 0s in it equal to the size of our pack, plus 1 (which we add so we can handle empty packs).
34
Sorry, but that C++14 code is something. I don't know what.
– James
Feb 20 at 18:34
14
@James It is a wonderful motivating example of why fold expressions in C++17 are very welcome. It, and similar tricks, turn out to be an efficient way to expand a pack "inplace" without any recursion and that compliers find easy to optimize.
– Yakk - Adam Nevraumont
Feb 20 at 19:10
3
@ruben multi line constexpr is illegal in 11
– Yakk - Adam Nevraumont
Feb 20 at 22:40
6
I can't see myself checking in that C++14 code. I'll stick to the C++11 one since I need that, anyway, but even if I could use it, the C++14 code requires so much explanation I wouldn't. These masks can always be written to have at most 32 elements, so I'm not worried about O(n^2) behavior. After all, if n is bounded by a constant, then it's really O(1). ;)
– Alex Reinking
Feb 21 at 7:30
8
To those trying to understand((1ull<<indexes)|...|0ull)
it is a "fold expression". Specifically it is a "binary right fold" and It should be parsed as(pack
op
...
op
init)
– Henrik Hansen
Feb 21 at 10:53
|
show 10 more comments
Best version is c++17:
template< unsigned char... indexes >
constexpr unsigned long long mask(){
return ((1ull<<indexes)|...|0ull);
}
Then
void apply_known_mask(std::bitset<64> &bits) {
constexpr auto m = mask<B,D,E,H,K,M,L,O>();
bits &= m;
}
back in c++14, we can do this strange trick:
template< unsigned char... indexes >
constexpr unsigned long long mask(){
auto r = 0ull;
using discard_t = int; // data never used
// value never used:
discard_t discard = {0,(void(
r |= (1ull << indexes) // side effect, used
),0)...};
(void)discard; // block unused var warnings
return r;
}
or, if we are stuck with c++11, we can solve it recursively:
constexpr unsigned long long mask(){
return 0;
}
template<class...Tail>
constexpr unsigned long long mask(unsigned char b0, Tail...tail){
return (1ull<<b0) | mask(tail...);
}
template< unsigned char... indexes >
constexpr unsigned long long mask(){
return mask(indexes...);
}
Godbolt with all 3 -- you can switch CPP_VERSION define, and get identical assembly.
In practice I'd use the most modern I could. 14 beats 11 because we don't have recursion and hence O(n^2) symbol length (which can explode compile time and compiler memory usage); 17 beats 14 because the compiler doesn't have to dead-code-eliminate that array, and that array trick is just ugly.
Of these 14 is the most confusing. Here we create an anonymous array of all 0s, meanwhile as a side effect construct our result, then discard the array. It has a number of 0s in it equal to the size of our pack, plus 1 (which we add so we can handle empty packs).
34
Sorry, but that C++14 code is something. I don't know what.
– James
Feb 20 at 18:34
14
@James It is a wonderful motivating example of why fold expressions in C++17 are very welcome. It, and similar tricks, turn out to be an efficient way to expand a pack "inplace" without any recursion and that compliers find easy to optimize.
– Yakk - Adam Nevraumont
Feb 20 at 19:10
3
@ruben multi line constexpr is illegal in 11
– Yakk - Adam Nevraumont
Feb 20 at 22:40
6
I can't see myself checking in that C++14 code. I'll stick to the C++11 one since I need that, anyway, but even if I could use it, the C++14 code requires so much explanation I wouldn't. These masks can always be written to have at most 32 elements, so I'm not worried about O(n^2) behavior. After all, if n is bounded by a constant, then it's really O(1). ;)
– Alex Reinking
Feb 21 at 7:30
8
To those trying to understand((1ull<<indexes)|...|0ull)
it is a "fold expression". Specifically it is a "binary right fold" and It should be parsed as(pack
op
...
op
init)
– Henrik Hansen
Feb 21 at 10:53
|
show 10 more comments
Best version is c++17:
template< unsigned char... indexes >
constexpr unsigned long long mask(){
return ((1ull<<indexes)|...|0ull);
}
Then
void apply_known_mask(std::bitset<64> &bits) {
constexpr auto m = mask<B,D,E,H,K,M,L,O>();
bits &= m;
}
back in c++14, we can do this strange trick:
template< unsigned char... indexes >
constexpr unsigned long long mask(){
auto r = 0ull;
using discard_t = int; // data never used
// value never used:
discard_t discard = {0,(void(
r |= (1ull << indexes) // side effect, used
),0)...};
(void)discard; // block unused var warnings
return r;
}
or, if we are stuck with c++11, we can solve it recursively:
constexpr unsigned long long mask(){
return 0;
}
template<class...Tail>
constexpr unsigned long long mask(unsigned char b0, Tail...tail){
return (1ull<<b0) | mask(tail...);
}
template< unsigned char... indexes >
constexpr unsigned long long mask(){
return mask(indexes...);
}
Godbolt with all 3 -- you can switch CPP_VERSION define, and get identical assembly.
In practice I'd use the most modern I could. 14 beats 11 because we don't have recursion and hence O(n^2) symbol length (which can explode compile time and compiler memory usage); 17 beats 14 because the compiler doesn't have to dead-code-eliminate that array, and that array trick is just ugly.
Of these 14 is the most confusing. Here we create an anonymous array of all 0s, meanwhile as a side effect construct our result, then discard the array. It has a number of 0s in it equal to the size of our pack, plus 1 (which we add so we can handle empty packs).
Best version is c++17:
template< unsigned char... indexes >
constexpr unsigned long long mask(){
return ((1ull<<indexes)|...|0ull);
}
Then
void apply_known_mask(std::bitset<64> &bits) {
constexpr auto m = mask<B,D,E,H,K,M,L,O>();
bits &= m;
}
back in c++14, we can do this strange trick:
template< unsigned char... indexes >
constexpr unsigned long long mask(){
auto r = 0ull;
using discard_t = int; // data never used
// value never used:
discard_t discard = {0,(void(
r |= (1ull << indexes) // side effect, used
),0)...};
(void)discard; // block unused var warnings
return r;
}
or, if we are stuck with c++11, we can solve it recursively:
constexpr unsigned long long mask(){
return 0;
}
template<class...Tail>
constexpr unsigned long long mask(unsigned char b0, Tail...tail){
return (1ull<<b0) | mask(tail...);
}
template< unsigned char... indexes >
constexpr unsigned long long mask(){
return mask(indexes...);
}
Godbolt with all 3 -- you can switch CPP_VERSION define, and get identical assembly.
In practice I'd use the most modern I could. 14 beats 11 because we don't have recursion and hence O(n^2) symbol length (which can explode compile time and compiler memory usage); 17 beats 14 because the compiler doesn't have to dead-code-eliminate that array, and that array trick is just ugly.
Of these 14 is the most confusing. Here we create an anonymous array of all 0s, meanwhile as a side effect construct our result, then discard the array. It has a number of 0s in it equal to the size of our pack, plus 1 (which we add so we can handle empty packs).
edited Feb 21 at 12:44
answered Feb 20 at 12:34
Yakk - Adam NevraumontYakk - Adam Nevraumont
186k19198380
186k19198380
34
Sorry, but that C++14 code is something. I don't know what.
– James
Feb 20 at 18:34
14
@James It is a wonderful motivating example of why fold expressions in C++17 are very welcome. It, and similar tricks, turn out to be an efficient way to expand a pack "inplace" without any recursion and that compliers find easy to optimize.
– Yakk - Adam Nevraumont
Feb 20 at 19:10
3
@ruben multi line constexpr is illegal in 11
– Yakk - Adam Nevraumont
Feb 20 at 22:40
6
I can't see myself checking in that C++14 code. I'll stick to the C++11 one since I need that, anyway, but even if I could use it, the C++14 code requires so much explanation I wouldn't. These masks can always be written to have at most 32 elements, so I'm not worried about O(n^2) behavior. After all, if n is bounded by a constant, then it's really O(1). ;)
– Alex Reinking
Feb 21 at 7:30
8
To those trying to understand((1ull<<indexes)|...|0ull)
it is a "fold expression". Specifically it is a "binary right fold" and It should be parsed as(pack
op
...
op
init)
– Henrik Hansen
Feb 21 at 10:53
|
show 10 more comments
34
Sorry, but that C++14 code is something. I don't know what.
– James
Feb 20 at 18:34
14
@James It is a wonderful motivating example of why fold expressions in C++17 are very welcome. It, and similar tricks, turn out to be an efficient way to expand a pack "inplace" without any recursion and that compliers find easy to optimize.
– Yakk - Adam Nevraumont
Feb 20 at 19:10
3
@ruben multi line constexpr is illegal in 11
– Yakk - Adam Nevraumont
Feb 20 at 22:40
6
I can't see myself checking in that C++14 code. I'll stick to the C++11 one since I need that, anyway, but even if I could use it, the C++14 code requires so much explanation I wouldn't. These masks can always be written to have at most 32 elements, so I'm not worried about O(n^2) behavior. After all, if n is bounded by a constant, then it's really O(1). ;)
– Alex Reinking
Feb 21 at 7:30
8
To those trying to understand((1ull<<indexes)|...|0ull)
it is a "fold expression". Specifically it is a "binary right fold" and It should be parsed as(pack
op
...
op
init)
– Henrik Hansen
Feb 21 at 10:53
34
34
Sorry, but that C++14 code is something. I don't know what.
– James
Feb 20 at 18:34
Sorry, but that C++14 code is something. I don't know what.
– James
Feb 20 at 18:34
14
14
@James It is a wonderful motivating example of why fold expressions in C++17 are very welcome. It, and similar tricks, turn out to be an efficient way to expand a pack "inplace" without any recursion and that compliers find easy to optimize.
– Yakk - Adam Nevraumont
Feb 20 at 19:10
@James It is a wonderful motivating example of why fold expressions in C++17 are very welcome. It, and similar tricks, turn out to be an efficient way to expand a pack "inplace" without any recursion and that compliers find easy to optimize.
– Yakk - Adam Nevraumont
Feb 20 at 19:10
3
3
@ruben multi line constexpr is illegal in 11
– Yakk - Adam Nevraumont
Feb 20 at 22:40
@ruben multi line constexpr is illegal in 11
– Yakk - Adam Nevraumont
Feb 20 at 22:40
6
6
I can't see myself checking in that C++14 code. I'll stick to the C++11 one since I need that, anyway, but even if I could use it, the C++14 code requires so much explanation I wouldn't. These masks can always be written to have at most 32 elements, so I'm not worried about O(n^2) behavior. After all, if n is bounded by a constant, then it's really O(1). ;)
– Alex Reinking
Feb 21 at 7:30
I can't see myself checking in that C++14 code. I'll stick to the C++11 one since I need that, anyway, but even if I could use it, the C++14 code requires so much explanation I wouldn't. These masks can always be written to have at most 32 elements, so I'm not worried about O(n^2) behavior. After all, if n is bounded by a constant, then it's really O(1). ;)
– Alex Reinking
Feb 21 at 7:30
8
8
To those trying to understand
((1ull<<indexes)|...|0ull)
it is a "fold expression". Specifically it is a "binary right fold" and It should be parsed as (pack
op
...
op
init)
– Henrik Hansen
Feb 21 at 10:53
To those trying to understand
((1ull<<indexes)|...|0ull)
it is a "fold expression". Specifically it is a "binary right fold" and It should be parsed as (pack
op
...
op
init)
– Henrik Hansen
Feb 21 at 10:53
|
show 10 more comments
The optimization you're looking for seems to be loop peeling, which is enabled at -O3
, or manually with -fpeel-loops
. I'm not sure why this falls under the purview of loop peeling rather than loop unrolling, but possibly it's unwilling to unroll a loop with nonlocal control flow inside it (as there is, potentially, from the range check).
By default, though, GCC stops short of being able to peel all the iterations, which apparently is necessary. Experimentally, passing -O2 -fpeel-loops --param max-peeled-insns=200
(the default value is 100) gets the job done with your original code: https://godbolt.org/z/NNWrga
You are amazing thank you! I had no idea this was configurable in GCC! Though for some reason-O3 -fpeel-loops --param max-peeled-insns=200
fails... It's due to-ftree-slp-vectorize
apparently.
– Alex Reinking
Feb 20 at 19:41
This solution seems to be limited to the x86-64 target. The output for ARM and ARM64 is still not pretty, which then again might be completely irrelevant for OP.
– realtime
Feb 21 at 9:24
@realtime - it is somewhat relevant, actually. Thanks for pointing out that it doesn't work in this case. Very disappointing that GCC doesn't catch it before being lowered to a platform-specific IR. LLVM optimizes it before any further lowering.
– Alex Reinking
Feb 21 at 11:37
add a comment |
The optimization you're looking for seems to be loop peeling, which is enabled at -O3
, or manually with -fpeel-loops
. I'm not sure why this falls under the purview of loop peeling rather than loop unrolling, but possibly it's unwilling to unroll a loop with nonlocal control flow inside it (as there is, potentially, from the range check).
By default, though, GCC stops short of being able to peel all the iterations, which apparently is necessary. Experimentally, passing -O2 -fpeel-loops --param max-peeled-insns=200
(the default value is 100) gets the job done with your original code: https://godbolt.org/z/NNWrga
You are amazing thank you! I had no idea this was configurable in GCC! Though for some reason-O3 -fpeel-loops --param max-peeled-insns=200
fails... It's due to-ftree-slp-vectorize
apparently.
– Alex Reinking
Feb 20 at 19:41
This solution seems to be limited to the x86-64 target. The output for ARM and ARM64 is still not pretty, which then again might be completely irrelevant for OP.
– realtime
Feb 21 at 9:24
@realtime - it is somewhat relevant, actually. Thanks for pointing out that it doesn't work in this case. Very disappointing that GCC doesn't catch it before being lowered to a platform-specific IR. LLVM optimizes it before any further lowering.
– Alex Reinking
Feb 21 at 11:37
add a comment |
The optimization you're looking for seems to be loop peeling, which is enabled at -O3
, or manually with -fpeel-loops
. I'm not sure why this falls under the purview of loop peeling rather than loop unrolling, but possibly it's unwilling to unroll a loop with nonlocal control flow inside it (as there is, potentially, from the range check).
By default, though, GCC stops short of being able to peel all the iterations, which apparently is necessary. Experimentally, passing -O2 -fpeel-loops --param max-peeled-insns=200
(the default value is 100) gets the job done with your original code: https://godbolt.org/z/NNWrga
The optimization you're looking for seems to be loop peeling, which is enabled at -O3
, or manually with -fpeel-loops
. I'm not sure why this falls under the purview of loop peeling rather than loop unrolling, but possibly it's unwilling to unroll a loop with nonlocal control flow inside it (as there is, potentially, from the range check).
By default, though, GCC stops short of being able to peel all the iterations, which apparently is necessary. Experimentally, passing -O2 -fpeel-loops --param max-peeled-insns=200
(the default value is 100) gets the job done with your original code: https://godbolt.org/z/NNWrga
answered Feb 20 at 16:04
SneftelSneftel
24.5k64379
24.5k64379
You are amazing thank you! I had no idea this was configurable in GCC! Though for some reason-O3 -fpeel-loops --param max-peeled-insns=200
fails... It's due to-ftree-slp-vectorize
apparently.
– Alex Reinking
Feb 20 at 19:41
This solution seems to be limited to the x86-64 target. The output for ARM and ARM64 is still not pretty, which then again might be completely irrelevant for OP.
– realtime
Feb 21 at 9:24
@realtime - it is somewhat relevant, actually. Thanks for pointing out that it doesn't work in this case. Very disappointing that GCC doesn't catch it before being lowered to a platform-specific IR. LLVM optimizes it before any further lowering.
– Alex Reinking
Feb 21 at 11:37
add a comment |
You are amazing thank you! I had no idea this was configurable in GCC! Though for some reason-O3 -fpeel-loops --param max-peeled-insns=200
fails... It's due to-ftree-slp-vectorize
apparently.
– Alex Reinking
Feb 20 at 19:41
This solution seems to be limited to the x86-64 target. The output for ARM and ARM64 is still not pretty, which then again might be completely irrelevant for OP.
– realtime
Feb 21 at 9:24
@realtime - it is somewhat relevant, actually. Thanks for pointing out that it doesn't work in this case. Very disappointing that GCC doesn't catch it before being lowered to a platform-specific IR. LLVM optimizes it before any further lowering.
– Alex Reinking
Feb 21 at 11:37
You are amazing thank you! I had no idea this was configurable in GCC! Though for some reason
-O3 -fpeel-loops --param max-peeled-insns=200
fails... It's due to -ftree-slp-vectorize
apparently.– Alex Reinking
Feb 20 at 19:41
You are amazing thank you! I had no idea this was configurable in GCC! Though for some reason
-O3 -fpeel-loops --param max-peeled-insns=200
fails... It's due to -ftree-slp-vectorize
apparently.– Alex Reinking
Feb 20 at 19:41
This solution seems to be limited to the x86-64 target. The output for ARM and ARM64 is still not pretty, which then again might be completely irrelevant for OP.
– realtime
Feb 21 at 9:24
This solution seems to be limited to the x86-64 target. The output for ARM and ARM64 is still not pretty, which then again might be completely irrelevant for OP.
– realtime
Feb 21 at 9:24
@realtime - it is somewhat relevant, actually. Thanks for pointing out that it doesn't work in this case. Very disappointing that GCC doesn't catch it before being lowered to a platform-specific IR. LLVM optimizes it before any further lowering.
– Alex Reinking
Feb 21 at 11:37
@realtime - it is somewhat relevant, actually. Thanks for pointing out that it doesn't work in this case. Very disappointing that GCC doesn't catch it before being lowered to a platform-specific IR. LLVM optimizes it before any further lowering.
– Alex Reinking
Feb 21 at 11:37
add a comment |
if using only C++11 is a must (&a)[N]
is a way to capture arrays. This allows you to write one single recursive function without using helper functions whatsoever:
template <std::size_t N>
constexpr std::uint64_t generate_mask(Flags const (&a)[N], std::size_t i = 0u){
return i < N ? (1ull << a[i] | generate_mask(a, i + 1u)) : 0ull;
}
assigning it to a constexpr auto
:
void apply_known_mask(std::bitset<64>& bits) {
constexpr const Flags important_bits = { B, D, E, H, K, M, L, O };
constexpr auto m = generate_mask(important_bits); //< here
bits &= m;
}
Test
int main() {
std::bitset<64> b;
b.flip();
apply_known_mask(b);
std::cout << b.to_string() << 'n';
}
Output
0000000000000000000000000000000000101110010000000000000100100100
// ^ ^^^ ^ ^ ^ ^
// O MLK H E D B
one really has to appreciate C++`s ability to calculate anything turing computable at compile time. It surely still blows my mind (<>).
For the later versions C++14 and C++17 yakk's answer already wonderfully covers that.
3
How does this demonstrate thatapply_known_mask
actually optimizes?
– Alex Reinking
Feb 20 at 21:50
2
@AlexReinking: All the scary bits areconstexpr
. And while that's theoretically not sufficient, we know that GCC is quite capable of evaluatingconstexpr
as intended.
– MSalters
Feb 21 at 12:37
add a comment |
if using only C++11 is a must (&a)[N]
is a way to capture arrays. This allows you to write one single recursive function without using helper functions whatsoever:
template <std::size_t N>
constexpr std::uint64_t generate_mask(Flags const (&a)[N], std::size_t i = 0u){
return i < N ? (1ull << a[i] | generate_mask(a, i + 1u)) : 0ull;
}
assigning it to a constexpr auto
:
void apply_known_mask(std::bitset<64>& bits) {
constexpr const Flags important_bits = { B, D, E, H, K, M, L, O };
constexpr auto m = generate_mask(important_bits); //< here
bits &= m;
}
Test
int main() {
std::bitset<64> b;
b.flip();
apply_known_mask(b);
std::cout << b.to_string() << 'n';
}
Output
0000000000000000000000000000000000101110010000000000000100100100
// ^ ^^^ ^ ^ ^ ^
// O MLK H E D B
one really has to appreciate C++`s ability to calculate anything turing computable at compile time. It surely still blows my mind (<>).
For the later versions C++14 and C++17 yakk's answer already wonderfully covers that.
3
How does this demonstrate thatapply_known_mask
actually optimizes?
– Alex Reinking
Feb 20 at 21:50
2
@AlexReinking: All the scary bits areconstexpr
. And while that's theoretically not sufficient, we know that GCC is quite capable of evaluatingconstexpr
as intended.
– MSalters
Feb 21 at 12:37
add a comment |
if using only C++11 is a must (&a)[N]
is a way to capture arrays. This allows you to write one single recursive function without using helper functions whatsoever:
template <std::size_t N>
constexpr std::uint64_t generate_mask(Flags const (&a)[N], std::size_t i = 0u){
return i < N ? (1ull << a[i] | generate_mask(a, i + 1u)) : 0ull;
}
assigning it to a constexpr auto
:
void apply_known_mask(std::bitset<64>& bits) {
constexpr const Flags important_bits = { B, D, E, H, K, M, L, O };
constexpr auto m = generate_mask(important_bits); //< here
bits &= m;
}
Test
int main() {
std::bitset<64> b;
b.flip();
apply_known_mask(b);
std::cout << b.to_string() << 'n';
}
Output
0000000000000000000000000000000000101110010000000000000100100100
// ^ ^^^ ^ ^ ^ ^
// O MLK H E D B
one really has to appreciate C++`s ability to calculate anything turing computable at compile time. It surely still blows my mind (<>).
For the later versions C++14 and C++17 yakk's answer already wonderfully covers that.
if using only C++11 is a must (&a)[N]
is a way to capture arrays. This allows you to write one single recursive function without using helper functions whatsoever:
template <std::size_t N>
constexpr std::uint64_t generate_mask(Flags const (&a)[N], std::size_t i = 0u){
return i < N ? (1ull << a[i] | generate_mask(a, i + 1u)) : 0ull;
}
assigning it to a constexpr auto
:
void apply_known_mask(std::bitset<64>& bits) {
constexpr const Flags important_bits = { B, D, E, H, K, M, L, O };
constexpr auto m = generate_mask(important_bits); //< here
bits &= m;
}
Test
int main() {
std::bitset<64> b;
b.flip();
apply_known_mask(b);
std::cout << b.to_string() << 'n';
}
Output
0000000000000000000000000000000000101110010000000000000100100100
// ^ ^^^ ^ ^ ^ ^
// O MLK H E D B
one really has to appreciate C++`s ability to calculate anything turing computable at compile time. It surely still blows my mind (<>).
For the later versions C++14 and C++17 yakk's answer already wonderfully covers that.
edited Feb 22 at 8:16
answered Feb 20 at 13:45
Stack DannyStack Danny
1,813623
1,813623
3
How does this demonstrate thatapply_known_mask
actually optimizes?
– Alex Reinking
Feb 20 at 21:50
2
@AlexReinking: All the scary bits areconstexpr
. And while that's theoretically not sufficient, we know that GCC is quite capable of evaluatingconstexpr
as intended.
– MSalters
Feb 21 at 12:37
add a comment |
3
How does this demonstrate thatapply_known_mask
actually optimizes?
– Alex Reinking
Feb 20 at 21:50
2
@AlexReinking: All the scary bits areconstexpr
. And while that's theoretically not sufficient, we know that GCC is quite capable of evaluatingconstexpr
as intended.
– MSalters
Feb 21 at 12:37
3
3
How does this demonstrate that
apply_known_mask
actually optimizes?– Alex Reinking
Feb 20 at 21:50
How does this demonstrate that
apply_known_mask
actually optimizes?– Alex Reinking
Feb 20 at 21:50
2
2
@AlexReinking: All the scary bits are
constexpr
. And while that's theoretically not sufficient, we know that GCC is quite capable of evaluating constexpr
as intended.– MSalters
Feb 21 at 12:37
@AlexReinking: All the scary bits are
constexpr
. And while that's theoretically not sufficient, we know that GCC is quite capable of evaluating constexpr
as intended.– MSalters
Feb 21 at 12:37
add a comment |
I would encourage you to write a proper EnumSet
type.
Writing a basic EnumSet<E>
in C++14 (onwards) based on std::uint64_t
is trivial:
template <typename E>
class EnumSet {
public:
constexpr EnumSet() = default;
constexpr EnumSet(std::initializer_list<E> values) {
for (auto e : values) {
set(e);
}
}
constexpr bool has(E e) const { return mData & mask(e); }
constexpr EnumSet& set(E e) { mData |= mask(e); return *this; }
constexpr EnumSet& unset(E e) { mData &= ~mask(e); return *this; }
constexpr EnumSet& operator&=(const EnumSet& other) {
mData &= other.mData;
return *this;
}
constexpr EnumSet& operator|=(const EnumSet& other) {
mData |= other.mData;
return *this;
}
private:
static constexpr std::uint64_t mask(E e) {
return std::uint64_t(1) << e;
}
std::uint64_t mData = 0;
};
This allows you to write simple code:
void apply_known_mask(EnumSet<Flags>& flags) {
static constexpr EnumSet<Flags> IMPORTANT{ B, D, E, H, K, M, L, O };
flags &= IMPORTANT;
}
In C++11, it requires some convolutions, but remains possible nonetheless:
template <typename E>
class EnumSet {
public:
template <E... Values>
static constexpr EnumSet make() {
return EnumSet(make_impl(Values...));
}
constexpr EnumSet() = default;
constexpr bool has(E e) const { return mData & mask(e); }
void set(E e) { mData |= mask(e); }
void unset(E e) { mData &= ~mask(e); }
EnumSet& operator&=(const EnumSet& other) {
mData &= other.mData;
return *this;
}
EnumSet& operator|=(const EnumSet& other) {
mData |= other.mData;
return *this;
}
private:
static constexpr std::uint64_t mask(E e) {
return std::uint64_t(1) << e;
}
static constexpr std::uint64_t make_impl() { return 0; }
template <typename... Tail>
static constexpr std::uint64_t make_impl(E head, Tail... tail) {
return mask(head) | make_impl(tail...);
}
explicit constexpr EnumSet(std::uint64_t data): mData(data) {}
std::uint64_t mData = 0;
};
And is invoked with:
void apply_known_mask(EnumSet<Flags>& flags) {
static constexpr EnumSet<Flags> IMPORTANT =
EnumSet<Flags>::make<B, D, E, H, K, M, L, O>();
flags &= IMPORTANT;
}
Even GCC trivially generates an and
instruction at -O1
godbolt:
apply_known_mask(EnumSet<Flags>&):
and QWORD PTR [rdi], 775946532
ret
1
In c++11 much of yourconstexpr
code isn't legal. I mean, some has 2 statements! (C++11 constexpr sucked)
– Yakk - Adam Nevraumont
Feb 20 at 17:12
@Yakk-AdamNevraumont: You did realize that I posted 2 versions of the code, the first for C++14 onward, and a second specially tailored for C++11? (to account for its limitations)
– Matthieu M.
Feb 20 at 17:29
1
It might be better to use std::underlying_type instead of std::uint64_t.
– James
Feb 20 at 18:32
@James: Actually, no. Do note thatEnumSet<E>
does not use a value ofE
as value directly, but instead uses1 << e
. It's a different domain altogether, which is actually what makes the class so valuable => no chance of accidentally indexing bye
instead of1 << e
.
– Matthieu M.
Feb 20 at 18:53
@MatthieuM. Yes you're right. I'm confusing it with our own implementation which is very similar to yours. The disadvantage of using (1 << e) is that if e is out of bounds for the underlying_type's size then it's probably UB, hopefully a compiler error.
– James
Feb 20 at 18:55
|
show 4 more comments
I would encourage you to write a proper EnumSet
type.
Writing a basic EnumSet<E>
in C++14 (onwards) based on std::uint64_t
is trivial:
template <typename E>
class EnumSet {
public:
constexpr EnumSet() = default;
constexpr EnumSet(std::initializer_list<E> values) {
for (auto e : values) {
set(e);
}
}
constexpr bool has(E e) const { return mData & mask(e); }
constexpr EnumSet& set(E e) { mData |= mask(e); return *this; }
constexpr EnumSet& unset(E e) { mData &= ~mask(e); return *this; }
constexpr EnumSet& operator&=(const EnumSet& other) {
mData &= other.mData;
return *this;
}
constexpr EnumSet& operator|=(const EnumSet& other) {
mData |= other.mData;
return *this;
}
private:
static constexpr std::uint64_t mask(E e) {
return std::uint64_t(1) << e;
}
std::uint64_t mData = 0;
};
This allows you to write simple code:
void apply_known_mask(EnumSet<Flags>& flags) {
static constexpr EnumSet<Flags> IMPORTANT{ B, D, E, H, K, M, L, O };
flags &= IMPORTANT;
}
In C++11, it requires some convolutions, but remains possible nonetheless:
template <typename E>
class EnumSet {
public:
template <E... Values>
static constexpr EnumSet make() {
return EnumSet(make_impl(Values...));
}
constexpr EnumSet() = default;
constexpr bool has(E e) const { return mData & mask(e); }
void set(E e) { mData |= mask(e); }
void unset(E e) { mData &= ~mask(e); }
EnumSet& operator&=(const EnumSet& other) {
mData &= other.mData;
return *this;
}
EnumSet& operator|=(const EnumSet& other) {
mData |= other.mData;
return *this;
}
private:
static constexpr std::uint64_t mask(E e) {
return std::uint64_t(1) << e;
}
static constexpr std::uint64_t make_impl() { return 0; }
template <typename... Tail>
static constexpr std::uint64_t make_impl(E head, Tail... tail) {
return mask(head) | make_impl(tail...);
}
explicit constexpr EnumSet(std::uint64_t data): mData(data) {}
std::uint64_t mData = 0;
};
And is invoked with:
void apply_known_mask(EnumSet<Flags>& flags) {
static constexpr EnumSet<Flags> IMPORTANT =
EnumSet<Flags>::make<B, D, E, H, K, M, L, O>();
flags &= IMPORTANT;
}
Even GCC trivially generates an and
instruction at -O1
godbolt:
apply_known_mask(EnumSet<Flags>&):
and QWORD PTR [rdi], 775946532
ret
1
In c++11 much of yourconstexpr
code isn't legal. I mean, some has 2 statements! (C++11 constexpr sucked)
– Yakk - Adam Nevraumont
Feb 20 at 17:12
@Yakk-AdamNevraumont: You did realize that I posted 2 versions of the code, the first for C++14 onward, and a second specially tailored for C++11? (to account for its limitations)
– Matthieu M.
Feb 20 at 17:29
1
It might be better to use std::underlying_type instead of std::uint64_t.
– James
Feb 20 at 18:32
@James: Actually, no. Do note thatEnumSet<E>
does not use a value ofE
as value directly, but instead uses1 << e
. It's a different domain altogether, which is actually what makes the class so valuable => no chance of accidentally indexing bye
instead of1 << e
.
– Matthieu M.
Feb 20 at 18:53
@MatthieuM. Yes you're right. I'm confusing it with our own implementation which is very similar to yours. The disadvantage of using (1 << e) is that if e is out of bounds for the underlying_type's size then it's probably UB, hopefully a compiler error.
– James
Feb 20 at 18:55
|
show 4 more comments
I would encourage you to write a proper EnumSet
type.
Writing a basic EnumSet<E>
in C++14 (onwards) based on std::uint64_t
is trivial:
template <typename E>
class EnumSet {
public:
constexpr EnumSet() = default;
constexpr EnumSet(std::initializer_list<E> values) {
for (auto e : values) {
set(e);
}
}
constexpr bool has(E e) const { return mData & mask(e); }
constexpr EnumSet& set(E e) { mData |= mask(e); return *this; }
constexpr EnumSet& unset(E e) { mData &= ~mask(e); return *this; }
constexpr EnumSet& operator&=(const EnumSet& other) {
mData &= other.mData;
return *this;
}
constexpr EnumSet& operator|=(const EnumSet& other) {
mData |= other.mData;
return *this;
}
private:
static constexpr std::uint64_t mask(E e) {
return std::uint64_t(1) << e;
}
std::uint64_t mData = 0;
};
This allows you to write simple code:
void apply_known_mask(EnumSet<Flags>& flags) {
static constexpr EnumSet<Flags> IMPORTANT{ B, D, E, H, K, M, L, O };
flags &= IMPORTANT;
}
In C++11, it requires some convolutions, but remains possible nonetheless:
template <typename E>
class EnumSet {
public:
template <E... Values>
static constexpr EnumSet make() {
return EnumSet(make_impl(Values...));
}
constexpr EnumSet() = default;
constexpr bool has(E e) const { return mData & mask(e); }
void set(E e) { mData |= mask(e); }
void unset(E e) { mData &= ~mask(e); }
EnumSet& operator&=(const EnumSet& other) {
mData &= other.mData;
return *this;
}
EnumSet& operator|=(const EnumSet& other) {
mData |= other.mData;
return *this;
}
private:
static constexpr std::uint64_t mask(E e) {
return std::uint64_t(1) << e;
}
static constexpr std::uint64_t make_impl() { return 0; }
template <typename... Tail>
static constexpr std::uint64_t make_impl(E head, Tail... tail) {
return mask(head) | make_impl(tail...);
}
explicit constexpr EnumSet(std::uint64_t data): mData(data) {}
std::uint64_t mData = 0;
};
And is invoked with:
void apply_known_mask(EnumSet<Flags>& flags) {
static constexpr EnumSet<Flags> IMPORTANT =
EnumSet<Flags>::make<B, D, E, H, K, M, L, O>();
flags &= IMPORTANT;
}
Even GCC trivially generates an and
instruction at -O1
godbolt:
apply_known_mask(EnumSet<Flags>&):
and QWORD PTR [rdi], 775946532
ret
I would encourage you to write a proper EnumSet
type.
Writing a basic EnumSet<E>
in C++14 (onwards) based on std::uint64_t
is trivial:
template <typename E>
class EnumSet {
public:
constexpr EnumSet() = default;
constexpr EnumSet(std::initializer_list<E> values) {
for (auto e : values) {
set(e);
}
}
constexpr bool has(E e) const { return mData & mask(e); }
constexpr EnumSet& set(E e) { mData |= mask(e); return *this; }
constexpr EnumSet& unset(E e) { mData &= ~mask(e); return *this; }
constexpr EnumSet& operator&=(const EnumSet& other) {
mData &= other.mData;
return *this;
}
constexpr EnumSet& operator|=(const EnumSet& other) {
mData |= other.mData;
return *this;
}
private:
static constexpr std::uint64_t mask(E e) {
return std::uint64_t(1) << e;
}
std::uint64_t mData = 0;
};
This allows you to write simple code:
void apply_known_mask(EnumSet<Flags>& flags) {
static constexpr EnumSet<Flags> IMPORTANT{ B, D, E, H, K, M, L, O };
flags &= IMPORTANT;
}
In C++11, it requires some convolutions, but remains possible nonetheless:
template <typename E>
class EnumSet {
public:
template <E... Values>
static constexpr EnumSet make() {
return EnumSet(make_impl(Values...));
}
constexpr EnumSet() = default;
constexpr bool has(E e) const { return mData & mask(e); }
void set(E e) { mData |= mask(e); }
void unset(E e) { mData &= ~mask(e); }
EnumSet& operator&=(const EnumSet& other) {
mData &= other.mData;
return *this;
}
EnumSet& operator|=(const EnumSet& other) {
mData |= other.mData;
return *this;
}
private:
static constexpr std::uint64_t mask(E e) {
return std::uint64_t(1) << e;
}
static constexpr std::uint64_t make_impl() { return 0; }
template <typename... Tail>
static constexpr std::uint64_t make_impl(E head, Tail... tail) {
return mask(head) | make_impl(tail...);
}
explicit constexpr EnumSet(std::uint64_t data): mData(data) {}
std::uint64_t mData = 0;
};
And is invoked with:
void apply_known_mask(EnumSet<Flags>& flags) {
static constexpr EnumSet<Flags> IMPORTANT =
EnumSet<Flags>::make<B, D, E, H, K, M, L, O>();
flags &= IMPORTANT;
}
Even GCC trivially generates an and
instruction at -O1
godbolt:
apply_known_mask(EnumSet<Flags>&):
and QWORD PTR [rdi], 775946532
ret
edited Feb 21 at 10:18
answered Feb 20 at 17:00
Matthieu M.Matthieu M.
204k29278517
204k29278517
1
In c++11 much of yourconstexpr
code isn't legal. I mean, some has 2 statements! (C++11 constexpr sucked)
– Yakk - Adam Nevraumont
Feb 20 at 17:12
@Yakk-AdamNevraumont: You did realize that I posted 2 versions of the code, the first for C++14 onward, and a second specially tailored for C++11? (to account for its limitations)
– Matthieu M.
Feb 20 at 17:29
1
It might be better to use std::underlying_type instead of std::uint64_t.
– James
Feb 20 at 18:32
@James: Actually, no. Do note thatEnumSet<E>
does not use a value ofE
as value directly, but instead uses1 << e
. It's a different domain altogether, which is actually what makes the class so valuable => no chance of accidentally indexing bye
instead of1 << e
.
– Matthieu M.
Feb 20 at 18:53
@MatthieuM. Yes you're right. I'm confusing it with our own implementation which is very similar to yours. The disadvantage of using (1 << e) is that if e is out of bounds for the underlying_type's size then it's probably UB, hopefully a compiler error.
– James
Feb 20 at 18:55
|
show 4 more comments
1
In c++11 much of yourconstexpr
code isn't legal. I mean, some has 2 statements! (C++11 constexpr sucked)
– Yakk - Adam Nevraumont
Feb 20 at 17:12
@Yakk-AdamNevraumont: You did realize that I posted 2 versions of the code, the first for C++14 onward, and a second specially tailored for C++11? (to account for its limitations)
– Matthieu M.
Feb 20 at 17:29
1
It might be better to use std::underlying_type instead of std::uint64_t.
– James
Feb 20 at 18:32
@James: Actually, no. Do note thatEnumSet<E>
does not use a value ofE
as value directly, but instead uses1 << e
. It's a different domain altogether, which is actually what makes the class so valuable => no chance of accidentally indexing bye
instead of1 << e
.
– Matthieu M.
Feb 20 at 18:53
@MatthieuM. Yes you're right. I'm confusing it with our own implementation which is very similar to yours. The disadvantage of using (1 << e) is that if e is out of bounds for the underlying_type's size then it's probably UB, hopefully a compiler error.
– James
Feb 20 at 18:55
1
1
In c++11 much of your
constexpr
code isn't legal. I mean, some has 2 statements! (C++11 constexpr sucked)– Yakk - Adam Nevraumont
Feb 20 at 17:12
In c++11 much of your
constexpr
code isn't legal. I mean, some has 2 statements! (C++11 constexpr sucked)– Yakk - Adam Nevraumont
Feb 20 at 17:12
@Yakk-AdamNevraumont: You did realize that I posted 2 versions of the code, the first for C++14 onward, and a second specially tailored for C++11? (to account for its limitations)
– Matthieu M.
Feb 20 at 17:29
@Yakk-AdamNevraumont: You did realize that I posted 2 versions of the code, the first for C++14 onward, and a second specially tailored for C++11? (to account for its limitations)
– Matthieu M.
Feb 20 at 17:29
1
1
It might be better to use std::underlying_type instead of std::uint64_t.
– James
Feb 20 at 18:32
It might be better to use std::underlying_type instead of std::uint64_t.
– James
Feb 20 at 18:32
@James: Actually, no. Do note that
EnumSet<E>
does not use a value of E
as value directly, but instead uses 1 << e
. It's a different domain altogether, which is actually what makes the class so valuable => no chance of accidentally indexing by e
instead of 1 << e
.– Matthieu M.
Feb 20 at 18:53
@James: Actually, no. Do note that
EnumSet<E>
does not use a value of E
as value directly, but instead uses 1 << e
. It's a different domain altogether, which is actually what makes the class so valuable => no chance of accidentally indexing by e
instead of 1 << e
.– Matthieu M.
Feb 20 at 18:53
@MatthieuM. Yes you're right. I'm confusing it with our own implementation which is very similar to yours. The disadvantage of using (1 << e) is that if e is out of bounds for the underlying_type's size then it's probably UB, hopefully a compiler error.
– James
Feb 20 at 18:55
@MatthieuM. Yes you're right. I'm confusing it with our own implementation which is very similar to yours. The disadvantage of using (1 << e) is that if e is out of bounds for the underlying_type's size then it's probably UB, hopefully a compiler error.
– James
Feb 20 at 18:55
|
show 4 more comments
Since C++11 you could also use classic TMP technique:
template<std::uint64_t Flag, std::uint64_t... Flags>
struct bitmask
{
static constexpr std::uint64_t mask =
bitmask<Flag>::value | bitmask<Flags...>::value;
};
template<std::uint64_t Flag>
struct bitmask<Flag>
{
static constexpr std::uint64_t value = (uint64_t)1 << Flag;
};
void apply_known_mask(std::bitset<64> &bits)
{
constexpr auto mask = bitmask<B, D, E, H, K, M, L, O>::value;
bits &= mask;
}
Link to Compiler Explorer: https://godbolt.org/z/Gk6KX1
The advantage of this approach over template constexpr function is that it's potentially slightly faster to compile due to rule of Chiel.
add a comment |
Since C++11 you could also use classic TMP technique:
template<std::uint64_t Flag, std::uint64_t... Flags>
struct bitmask
{
static constexpr std::uint64_t mask =
bitmask<Flag>::value | bitmask<Flags...>::value;
};
template<std::uint64_t Flag>
struct bitmask<Flag>
{
static constexpr std::uint64_t value = (uint64_t)1 << Flag;
};
void apply_known_mask(std::bitset<64> &bits)
{
constexpr auto mask = bitmask<B, D, E, H, K, M, L, O>::value;
bits &= mask;
}
Link to Compiler Explorer: https://godbolt.org/z/Gk6KX1
The advantage of this approach over template constexpr function is that it's potentially slightly faster to compile due to rule of Chiel.
add a comment |
Since C++11 you could also use classic TMP technique:
template<std::uint64_t Flag, std::uint64_t... Flags>
struct bitmask
{
static constexpr std::uint64_t mask =
bitmask<Flag>::value | bitmask<Flags...>::value;
};
template<std::uint64_t Flag>
struct bitmask<Flag>
{
static constexpr std::uint64_t value = (uint64_t)1 << Flag;
};
void apply_known_mask(std::bitset<64> &bits)
{
constexpr auto mask = bitmask<B, D, E, H, K, M, L, O>::value;
bits &= mask;
}
Link to Compiler Explorer: https://godbolt.org/z/Gk6KX1
The advantage of this approach over template constexpr function is that it's potentially slightly faster to compile due to rule of Chiel.
Since C++11 you could also use classic TMP technique:
template<std::uint64_t Flag, std::uint64_t... Flags>
struct bitmask
{
static constexpr std::uint64_t mask =
bitmask<Flag>::value | bitmask<Flags...>::value;
};
template<std::uint64_t Flag>
struct bitmask<Flag>
{
static constexpr std::uint64_t value = (uint64_t)1 << Flag;
};
void apply_known_mask(std::bitset<64> &bits)
{
constexpr auto mask = bitmask<B, D, E, H, K, M, L, O>::value;
bits &= mask;
}
Link to Compiler Explorer: https://godbolt.org/z/Gk6KX1
The advantage of this approach over template constexpr function is that it's potentially slightly faster to compile due to rule of Chiel.
answered Feb 20 at 16:40
Michał ŁośMichał Łoś
453311
453311
add a comment |
add a comment |
There are some far to 'clever' ideas here. You are probably not helping maintainability by following them.
is
{B, D, E, H, K, M, L, O};
so much easier to write than
(B| D| E| H| K| M| L| O);
?
Then none of the rest of the code is needed.
1
"B", "D", etc are not flags themselves.
– Michał Łoś
Feb 22 at 12:01
Yes, you would need to transform these to flags first. That's not at all clear in my answer. sorry. I will update.
– ANone
Feb 22 at 12:08
add a comment |
There are some far to 'clever' ideas here. You are probably not helping maintainability by following them.
is
{B, D, E, H, K, M, L, O};
so much easier to write than
(B| D| E| H| K| M| L| O);
?
Then none of the rest of the code is needed.
1
"B", "D", etc are not flags themselves.
– Michał Łoś
Feb 22 at 12:01
Yes, you would need to transform these to flags first. That's not at all clear in my answer. sorry. I will update.
– ANone
Feb 22 at 12:08
add a comment |
There are some far to 'clever' ideas here. You are probably not helping maintainability by following them.
is
{B, D, E, H, K, M, L, O};
so much easier to write than
(B| D| E| H| K| M| L| O);
?
Then none of the rest of the code is needed.
There are some far to 'clever' ideas here. You are probably not helping maintainability by following them.
is
{B, D, E, H, K, M, L, O};
so much easier to write than
(B| D| E| H| K| M| L| O);
?
Then none of the rest of the code is needed.
answered Feb 22 at 11:51
ANoneANone
712
712
1
"B", "D", etc are not flags themselves.
– Michał Łoś
Feb 22 at 12:01
Yes, you would need to transform these to flags first. That's not at all clear in my answer. sorry. I will update.
– ANone
Feb 22 at 12:08
add a comment |
1
"B", "D", etc are not flags themselves.
– Michał Łoś
Feb 22 at 12:01
Yes, you would need to transform these to flags first. That's not at all clear in my answer. sorry. I will update.
– ANone
Feb 22 at 12:08
1
1
"B", "D", etc are not flags themselves.
– Michał Łoś
Feb 22 at 12:01
"B", "D", etc are not flags themselves.
– Michał Łoś
Feb 22 at 12:01
Yes, you would need to transform these to flags first. That's not at all clear in my answer. sorry. I will update.
– ANone
Feb 22 at 12:08
Yes, you would need to transform these to flags first. That's not at all clear in my answer. sorry. I will update.
– ANone
Feb 22 at 12:08
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f54786278%2fhow-do-i-write-a-maintainable-fast-compile-time-bit-mask-in-c%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
4
Rather than using a loop, can't you construct a mask with
B | D | E | ... | O
?– HolyBlackCat
Feb 20 at 12:25
4
The enum has bit positions rather than already expanded bits, so I could do
(1ULL << B) | ... | (1ULL << O)
– Alex Reinking
Feb 20 at 12:26
3
The downside being that the actual names are long and irregular and it's not nearly as easy to see which flags are in the mask with all that line noise.
– Alex Reinking
Feb 20 at 12:32
2
@AlexReinking You could make it one
(1ULL << Constant)
| per line, and align the constant names on the different lines, that would be easier on the eyes.– einpoklum
Feb 21 at 15:37