简体   繁体   English

C++ 20:std::array 作为非类型模板参数重新洗牌元素

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

I recently implemented a Builder class, but I wanted to avoid throwing exceptions.我最近实现了一个 Builder class,但我想避免抛出异常。 So I had an idea that I could parameterise the Builder with an array of bools representing which fields have been set.所以我有一个想法,我可以用一组表示已设置哪些字段的布尔值对 Builder 进行参数化。 Each setter would return a new specialisation of the Builder with the corresponding field flag set.每个 setter 都将返回 Builder 的新特化,并设置了相应的字段标志。 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.事实证明,作为非类型模板 arguments 的复杂数据类型仅在 C++ 20 中可用。但我还是尝试了它。

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:随着每个新的专业化返回,“真”标志在开始时聚集在一起,如此示例调试 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?这只是我以某种方式丢失的 C++ 20 的一个微妙之处,还是 gcc 中的一个错误?

I checked this with gcc 9.3.0 and gcc 10.2.0.我用 gcc 9.3.0 和 gcc 10.2.0 检查了这个。 I also tried compiling from git, version 11.0.1 change a18ebd6c439.我还尝试从 git 编译,版本 11.0.1 更改 a18ebd6c439。 Command line was g++ -Wall --std=c++2a builder.cpp .命令行是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.我还在 gcc 的 bugzilla 中进行了搜索,但找不到任何看起来相似的内容。

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 .我已经使用https://godbolt.org/来检查为多个编译器生成的代码,这确实是 gcc 中的一个错误 Both clang and msvc produce correct results. clang 和 msvc 都产生正确的结果。

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.这是有趣的部分,为Builder<std::array<bool, 2ul>{}>::SetSecond()方法生成的汇编程序会在您的较短示例中导致错误。 The actual code is not that important, the error can be seen by looking at the types:实际代码并不那么重要,通过查看类型可以看出错误:

Clang produces (correctly) this: Clang 产生(正确)这个:

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: GCC 产生(错误地)这个:

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} .如果您比较被call的 function 的类型,您可以清楚地看到在 gcc 中, SetSecond()没有设置第二个 - 有{true} ,但应该是{false, true}

So, time to switch to clang?那么,是时候切换到 clang 了吗?

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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