【问题标题】:C++ 20: std::array as non-type template argument reshuffles elementsC++ 20:std::array 作为非类型模板参数重新洗牌元素
【发布时间】:2021-06-05 18:07:49
【问题描述】:

我最近实现了一个 Builder 类,但我想避免抛出异常。所以我有一个想法,我可以用一组表示已设置哪些字段的布尔值对 Builder 进行参数化。每个 setter 都将返回 Builder 的新特化,并设置了相应的字段标志。这样我就可以在编译时检查是否设置了正确的字段。

事实证明,作为非类型模板参数的复杂数据类型仅在 C++ 20 中可用。但我还是尝试了它。

事实证明它以一种奇怪的方式行为不端。当返回每个新的特化时,“真”标志会在开始时聚集在一起,如以下示例调试输出所示:

 - 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

这些来自下面两行中的第二行。删除第一个可以解决问题,这表明第一个实例化以某种方式影响了第二个。

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();

它应该这样做吗?这只是我以某种方式遗漏的 C++ 20 的一个微妙之处,还是 gcc 中的一个错误?

我使用 gcc 9.3.0 和 gcc 10.2.0 对此进行了检查。我还尝试从 git 编译,版本 11.0.1 更改 a18ebd6c439。命令行是g++ -Wall --std=c++2a builder.cpp。它们的行为方式都相同。我也在 gcc 的 bugzilla 中搜索过,但找不到任何类似的东西。

以下是两个代码示例。首先,尽可能地剥离一个版本以显示问题。第二个显示了我试图实现的更多背景。 (还有第三个更现实的版本,但公开发布可能会有问题。)

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

【问题讨论】:

    标签: c++ templates g++ c++20


    【解决方案1】:

    我使用https://godbolt.org/ 来检查为多个编译器生成的代码,这确实是 gcc 中的一个错误。 clang 和 msvc 都产生正确的结果。

    这是有趣的部分,为方法 Builder&lt;std::array&lt;bool, 2ul&gt;{}&gt;::SetSecond() 生成的汇编程序会在您的较短示例中导致错误。实际代码没那么重要,看类型就能看出错误:

    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
    
    

    如果对比得到called的函数的类型,可以清楚的看到在gcc中,SetSecond()没有设置第二个——有{true},但应该是{false, true}

    那么,是时候切换到 clang 了吗?

    【讨论】:

    • 哈!实际上我通常在 clang 中工作,但是当我了解到直到 C++ 20 才支持此功能时,我不知何故觉得 g++ 是试验 C++ 20 的最简单方法。事实上,这一切都没有实际意义:在现实生活中我无论如何,我都坚持使用 C++ 14。但至少有了这个证据,我可以向 gcc 的 bugzilla 提交错误报告。所以谢谢!
    • 在这里向 gcc 报告了错误:gcc.gnu.org/bugzilla/show_bug.cgi?id=99460
    猜你喜欢
    • 1970-01-01
    • 2021-03-20
    • 1970-01-01
    • 2023-02-05
    • 2021-02-07
    • 2020-05-02
    • 2020-12-20
    • 2016-10-20
    • 1970-01-01
    相关资源
    最近更新 更多