简体   繁体   中英

C++ 20: std::array as non-type template argument reshuffles elements

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;
}

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?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM