【问题标题】:Storing end pointer to uninitialized array in constexpr context在 constexpr 上下文中存储指向未初始化数组的结束指针
【发布时间】:2022-08-15 11:44:55
【问题描述】:

我正在尝试创建一个 constexpr 友好的小型缓冲区优化向量类型,它像往常一样存储开始、结束和容量指针,但是当它默认构造时,开始和容量指针指向本地内存,然后在需要时重新分配到堆。但是我无法弄清楚如何在 constexpr 上下文中存储容量指针,因为我希望它能够支持非平凡构造的类型。为了存储非平凡构造的类型,我不能使用 std::aligned_storage 因为完整的类型擦除不允许我以后获取元素(因为不允许 reinterpret_cast ),所以我决定将元素存储在数组中工会(有点像可选的)。像这样存储它可以让我稍后通过联合访问获取元素,并允许我保持数组未初始化,但是当它存储在联合内部时,我无法弄清楚如何存储指向容量结束的值指针,因为它没有检测到指向联合值时,整个指针超出最后一个指针的内容。当然,所有这些都需要 c++20。

#include <algorithm>
#include <memory>
#include <utility>
#include <array>

struct Null{};

template<typename T>
union Storage
{
    Null uninitialized;
    T value;

    constexpr Storage()
    : uninitialized{}
    {}

    template<typename... Args>
    constexpr Storage(Args&&... args)
    : value(std::forward<Args>(args)...)
    {}

    constexpr ~Storage(){}
};

template<typename T, size_t N>
struct Vec
{
    std::array<Storage<T>, N> storage;

    T* begin; 
    T* end;   

    constexpr Vec()
    : begin(makeBegin())
    , end(makeEnd())
    {}

    constexpr T* makeBegin()
    {
        return &storage[0].value;
    }
    constexpr T* makeEnd()
    {
        return (&storage[N].value);
    }
};


constexpr bool test()
{
    Vec<std::pair<float, float>, 10> vec{};

    for(auto it = vec.begin; it < vec.end; ++it)
    {
        std::construct_at(it, 10.0f, 10.0f);
    }

    return vec.begin[5] == std::pair{ 10.0f, 10.0f };
}


int main()
{
    static_assert(test());
}

https://godbolt.org/z/46o19qcvP

是否有另一种方法可以获取指向存储的非平凡可构造类型的指针,例如数组中的对而不初始化它们?

  • (1) 任何地方都没有T 的数组,所以通过Vec&lt;T&gt;::begin 的指针运算和索引是UB。 (2) 允许实现向Storage 添加它认为合适的填充。 option 不在乎,但你不能便携地使用相同的技巧来制作数组。
  • 我当然可以将指向联合本身的指针保留为迭代器,但是与后来使用的堆内存迭代器结合起来会有点麻烦,以防它溢出。有什么办法可以将它存储为 T 指针?
  • 我不认为这是可行的。
  • 我不确定您为什么要使用 Storage union 而不是 std::variant,其中定义的第一个类型是 std::monostate。
  • Variant 也存储了一个我不需要的类型索引。但是,如果我有一个变体数组,我仍然无法让迭代器指向 T。似乎在 constexpr 中使用未初始化的数组比使用 constexpr new 更难......这将迫使我编写 2 个实现,一个用于 consteval,一个用于非 consteval

标签: c++ arrays c++20 constexpr unions


【解决方案1】:

您的代码存在多个问题:

  • T* 不是您的表示的有效迭代器,因为 Ts 实际上是结构的成员。迭代器需要对数组的值类型进行操作。
  • 使用storage[N] 是越界访问,即使您只是尝试使用其中成员的地址。

解决这两个问题的一种方法是使用自定义迭代器类型。这是一个基于您的原始代码的示例(迭代器实现有点不竞争 - 我刚刚实现了编译代码所需的内容):

#include <algorithm>
#include <memory>
#include <utility>
#include <array>


template<typename T>
union Storage
{
    T value;

    constexpr Storage() {}

    template<typename... Args>
    constexpr Storage(Args&&... args)
    : value(std::forward<Args>(args)...)
    {}

    constexpr ~Storage(){}
};

template<typename T, size_t N>
struct Vec
{
    std::array<Storage<T>, N> storage;

    struct iterator {
        Storage<T>* p;
        constexpr T& operator*() { return this->p->value; }
        constexpr T* operator->() { return &this->p->value; }
        constexpr T& operator[](std::size_t n) { return (this->p)[n].value; }
        constexpr iterator& operator++() { ++this->p; return *this; }
        constexpr iterator  operator++(int) { auto rc(*this); ++*this; return rc; }
        constexpr iterator& operator+= (std::size_t n){ this->p += n; return *this; }
        friend constexpr iterator operator+ (iterator it, std::size_t n) { return it += n; }
        constexpr bool      operator== (iterator const&) const = default;
    constexpr bool      operator< (iterator const& other) const { return this->p < other.p; }
    };
    
    
    iterator begin; 
    iterator end;   
    
    constexpr Vec()
    : begin(makeBegin())
    , end(makeEnd())
    {}

    constexpr iterator makeBegin()
    {
        return {&this->storage[0]};
    }
    constexpr iterator makeEnd()
    {
        return this->makeBegin() + N;
    }
};


constexpr bool test()
{
    Vec<std::pair<float, float>, 10> vec{};

    for(auto it = vec.begin; it < vec.end; ++it)
    {
        std::construct_at(&*it, 10.0f, 10.0f);
    }

    return vec.begin[5] == std::pair{ 10.0f, 10.0f };
}


int main()
{   
    static_assert(test());
}

由于需要单独创建 T 元素才能成为活动的 union 成员,因此这里并不是真正存储 T* 对象的方法。这意味着向量的一个可能大小可变的部分还存储Storage&lt;T&gt; 元素,以避免混淆两种不同的迭代器类型。

【讨论】:

  • Problem。我不知道标准在哪里这么说。
  • @n.1.8e9-where's-my-sharem.:我明白了。我只在编译的地方用 gcc-11.2.0 进行了测试。错误消息提出了如何启用union 成员。我怀疑通过构造它是union 首先应该禁止的。可能最好的方法是使用我原来的自定义迭代器方法,并使外部范围也使用相同的union 成员:这不会遇到与每个单独的构造激活union 成员相同的问题。
  • 您不必在这里一次性初始化整个数组吗?我不认为你可以在这里一次初始化一个数组元素。
  • @AndreasLoanjoe:是的,我想我同意。我将使用自定义迭代器恢复我的答案。当然,这意味着数组的变量部分也将使用union 表示,这有点烦人,但可能会使元素的整体处理更加一致。
  • 有点令人失望,但我认为这是正确的答案。我想我会在 constexpr 上下文中使用 consteval new 和 delete 。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-10-11
  • 2012-11-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-28
相关资源
最近更新 更多