Reputation: 487
I recently implemented a Builder class, but I wanted to avoid throwing exceptions. So I had an idea that I could parameterise the Builder with an array of bools representing which fields have been set. Each setter would return a new specialisation of the Builder with the corresponding field flag set. That way I could check that the right fields were set at compile time.
It turns out that complex data types as non-type template arguments is only available in C++ 20. But I experimented with it anyway.
It turns out it misbehaves in a strange way. As each new specialisation is returned, the "true" flags bunch up towards the start, as shown in this sample debug output:
- set field 4 old flags 00000 new flags 00001
- set field 2 old flags 10000 new flags 10100
- set field 0 old flags 11000 new flags 11000
- set field 3 old flags 11000 new flags 11010
- set field 1 old flags 11100 new flags 11100
Those are from the second of the two lines below. Removing the first fixes the problem, suggesting that the first instantiation is somehow affecting the second.
Fields fields1 = Builder().SetFirst(1).SetSecond(2).SetThird(3).SetFourth(4).SetFifth(5).Build();
Fields fields2 = Builder().SetFifth(5).SetThird(3).SetFirst(1).SetFourth(4).SetSecond(2).Build();
Is it supposed to do that? Is this just a subtlety of C++ 20 that I'm missing somehow, or is it a bug in gcc?
I checked this with gcc 9.3.0 and gcc 10.2.0. I also tried compiling from git, version 11.0.1 change a18ebd6c439. Command line was g++ -Wall --std=c++2a builder.cpp
. They all behave the same way. I also searched in gcc's bugzilla, but couldn't find anything that looked similar.
Below are two code samples. First a version stripped back as far as I could to show the problem. The second shows more context on what I was trying to achieve. (There's a third, more realistic version, but it might be a problem to post that in public.)
#include <array>
#include <cassert>
using Flags = std::array<bool, 2>;
template<Flags flags = Flags{}>
class Builder
{
public:
Builder() {
}
auto SetFirst() {
constexpr auto new_flags = SetFieldFlag<0>();
Builder<new_flags> new_builder;
return new_builder;
}
auto SetSecond() {
constexpr auto new_flags = SetFieldFlag<1>();
Builder<new_flags> new_builder;
return new_builder;
}
Flags GetFlags() const {
return flags;
}
private:
template<int field>
static constexpr auto SetFieldFlag() {
auto new_flags = flags;
std::get<field>(new_flags) = true;
return new_flags;
}
};
int main()
{
auto flags1 = Builder().SetFirst().SetSecond().GetFlags();
assert(flags1[0]);
assert(flags1[1]);
auto flags2 = Builder().SetSecond().SetFirst().GetFlags();
assert(flags2[0]);
assert(flags2[1]);
return 0;
}
#include <iostream>
#include <array>
constexpr int NumFields = 5;
using Flags = std::array<bool, NumFields>;
using Fields = std::array<int, NumFields>;
std::ostream& operator<<(std::ostream& out, Flags flags) {
for (int i = 0; i < NumFields; ++i) {
out << flags[i];
}
return out;
}
std::ostream& operator<<(std::ostream& out, Fields fields) {
for (int i = 0; i < NumFields; ++i) {
out << (i ? ":" : "") << fields[i];
}
return out;
}
template<Flags flags = Flags{}>
class Builder
{
public:
Builder(Fields fields_in = Fields{})
: fields(fields_in) {
}
auto SetFirst(int value) {
fields.at(0) = value;
return BuilderWithField<0>();
}
auto SetSecond(int value) {
fields.at(1) = value;
return BuilderWithField<1>();
}
auto SetThird(int value) {
fields.at(2) = value;
return BuilderWithField<2>();
}
auto SetFourth(int value) {
fields.at(3) = value;
return BuilderWithField<3>();
}
auto SetFifth(int value) {
fields.at(4) = value;
return BuilderWithField<4>();
}
Fields Build() {
std::cout << " - build with flags " << flags << std::endl;
static_assert(std::get<0>(flags), "first field not set");
static_assert(std::get<1>(flags), "second field not set");
static_assert(std::get<2>(flags), "third field not set");
static_assert(std::get<3>(flags), "fourth field not set");
static_assert(std::get<4>(flags), "fifth field not set");
return fields;
}
private:
template<int field>
static constexpr auto SetFieldFlag() {
auto new_flags = flags;
std::get<field>(new_flags) = true;
return new_flags;
}
template<int field>
auto BuilderWithField() {
constexpr auto new_flags = SetFieldFlag<field>();
std::cout << " - set field " << field << " old flags " << flags << " new flags " << new_flags << std::endl;
Builder<new_flags> new_builder(fields);
return new_builder;
}
Fields fields;
};
int main()
{
Fields fields1 = Builder().SetFirst(1).SetSecond(2).SetThird(3).SetFourth(4).SetFifth(5).Build();
std::cout << fields1 << std::endl;
Fields fields2 = Builder().SetFifth(5).SetThird(3).SetFirst(1).SetFourth(4).SetSecond(2).Build();
std::cout << fields2 << std::endl;
return 0;
}
Upvotes: 3
Views: 182
Reputation: 1868
I have used https://godbolt.org/ to examine the generated code for multiple compilers and this is indeed a bug in gcc. Both clang and msvc produce correct results.
Here's the interesting part, the assembler generated for the method Builder<std::array<bool, 2ul>{}>::SetSecond()
which causes the error in your shorter example. The actual code is not that important, the error can be seen by looking at the types:
Clang produces (correctly) this:
Builder<std::array<bool, 2ul>{}>::SetSecond(): # @Builder<std::array<bool, 2ul>{}>::SetSecond()
push rbp
mov rbp, rsp
sub rsp, 32
mov qword ptr [rbp - 8], rdi
mov ax, word ptr [.L__const.Builder<std::array<bool, 2ul>{}>::SetSecond().new_flags]
mov word ptr [rbp - 16], ax
lea rdi, [rbp - 24]
call Builder<std::array<bool, 2ul>{bool [2]{false, true}}>::Builder() [base object constructor]
add rsp, 32
pop rbp
ret
GCC produces (incorrectly) this:
Builder<std::array<bool, 2ul>{}>::SetSecond():
push rbp
mov rbp, rsp
push rbx
sub rsp, 40
mov QWORD PTR [rbp-40], rdi
mov WORD PTR [rbp-18], 0
mov BYTE PTR [rbp-17], 1
lea rax, [rbp-19]
mov rdi, rax
call Builder<std::array<bool, 2ul>{bool [2]{true}}>::Builder() [complete object constructor]
nop
mov eax, ebx
mov rbx, QWORD PTR [rbp-8]
leave
ret
If you compare the type of the function that gets call
ed, you can clearly see that in gcc, SetSecond()
did not set the second -- there's {true}
, but should be {false, true}
.
So, time to switch to clang?
Upvotes: 4