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

3

我最近实现了一个构建器类,但我想避免抛出异常。所以我想到了一个方法,可以使用一个布尔数组作为参数化的构建器,表示哪些字段已经设置。每次setter方法都会返回一个新的构建器的特殊版本,并将相应的字段标志设置。这样,我就可以在编译时检查是否设置了正确的字段。

结果发现,在C++20中,复杂数据类型作为非类型模板参数才可用。但是我还是进行了一些实验。

结果发现它的行为很奇怪。随着每个新的特殊版本被返回,“true”标志会向开始位置聚集,如下面的示例调试输出所示:

 - 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进行了检查。我还尝试从版本11.0.1更改a18ebd6c439编译。命令行是g++ -Wall --std=c++2a builder.cpp。它们的行为都相同。我还搜索了gcc的报告系统,但找不到类似的问题。

以下是两个代码示例。首先是裁剪后的版本,以显示问题。第二个示例显示了我试图实现的更多上下文。 (还有一个更现实的第三个版本,但在公共场合发布可能会出问题。)

#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;
}
1个回答

4

我使用https://godbolt.org/来检查多个编译器生成的代码,确实是gcc的一个bug。Clang和MSVC都能够产生正确的结果。

有趣的部分在于,在您较短的示例中导致错误的方法Builder<std::array<bool, 2ul>{}>::SetSecond()生成的汇编程序。实际代码并不那么重要,可以通过查看类型来看到错误:

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错误地产生了以下输出:

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


如果你比较被call的函数的类型,你可以明显地看到,在gcc中,SetSecond()没有设置秒钟-有{true},但应该是{false, true}
那么,是时候转换到clang了吗?

哈!其实我通常使用clang,但当我得知这个特性直到C++ 20才被支持时,我就有了g++是尝试C++ 20的最简单方法的印象。事实上这都是无意义的:在现实生活中,我仍然被困在C++ 14中。但至少有了这个证据,我可以向gcc的bugzilla提交一个错误报告。所以谢谢! - Matthew Exon
1
错误已经报告给gcc,链接在这里:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=99460 - Matthew Exon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接