【问题标题】:Is there a standard C++ class for arrays with fixed run-time-determined size?是否有用于具有固定运行时确定大小的数组的标准 C++ 类?
【发布时间】:2019-08-17 23:23:36
【问题描述】:

我需要一个运行时已知大小且无需调整大小的容器。 std::unique_ptr<T[]> 会很有用,但没有封装的 size 成员。同时std::array 仅用于编译类型大小。因此,我需要这些类的一些组合,而没有/最小的开销。

是否有满足我需要的标准类,也许在即将到来的 C++20 中?

【问题讨论】:

  • 您指的是类型界限还是大小界限?对于后者,您可以预先分配一个vector。那应该说unique吗?
  • @Carcigenicate “类型界限”是什么意思? vector 有过多的接口,我只想拥有运行时大小的std::array。例如。 operator[]size() 一见钟情。
  • 有什么问题?如果除了保留之外还想初始化元素,只需调用std::vector::resize即可。
  • @αλεχολυτ 没有容器允许您访问不存在的元素。调用new T[n] 初始化n Ts
  • 当您没有计划调整大小时,存储在std::vector 中的容量编号似乎是一种浪费。但与动态分配的时间和空间成本相比,它的成本可以忽略不计。因此,与仅使用std::vector 相比,使用不可调整大小的动态数组类并不能真正为您带来任何有形的东西。 (我很难学到这一点)。

标签: c++ arrays c++20


【解决方案1】:

使用std::vector。这是 STL 中运行时大小数组的类。

它可以让你调整它的大小或将元素推入它:

auto vec = std::vector<int>{};

vec.resize(10); // now vector has 10 ints 0 initialized
vec.push_back(1); // now 11 ints

cmets中提到的一些问题:

vector 接口过多

std::array 也是如此。 std::array 包括运算符在内有 20 多个函数。

只是不要使用你不需要的东西。您无需为不会使用的功能付费。它甚至不会增加您的二进制文件大小。

vector 将在调整大小时强制初始化项目。据我所知,对于 >= size 的索引,不允许使用 operator[](尽管调用了 reserve)。

这不是它的用途。保留时,您应该使用resize 或通过将元素推入其中来调整向量的大小。你说vector会强制将元素初始化到其中,但问题是你不能在非构造对象上调用operator=,包括int。

这是一个使用储备的例子:

auto vec = std::vector<int>{};

vec.reserve(10); // capacity of at least 10
vec.resize(3); // Contains 3 zero initialized ints.

// If you don't want to `force` initialize elements
// you should push or emplace element into it:

vec.emplace_back(1); // no reallocation for the three operations.
vec.emplace_back(2); // no default initialize either.
vec.emplace_back(3); // ints constructed with arguments in emplace_back

请记住,这种分配和用例的可能性很高,编译器可能会完全忽略向量中元素的构造。您的代码中可能没有开销。

如果您的代码符合非常精确的性能规范,我建议您measureprofile。如果您没有这样的规范,很可能这是过早的优化。内存分配的成本完全衡量的是逐个初始化元素所花费的时间。

您的程序的其他部分可能会被重构以获得比简单的初始化所能提供的更多的性能。事实上,妨碍它可能会阻碍优化并使您的程序变慢。

【讨论】:

  • 您如何建议只初始化 10 个元素向量中的第 5 个元素?现在关于开销,存储唯一的大小和指向数据的指针将在 x64 堆栈上占用 2*sizeof(size_t) = 16 个字节。同时,大多数流行的编译器使用 24 字节来表示 std::vector。 8个字节不算多,但我不需要。
  • @αλεχολυτ:定义“初始化只说第 5 个元素”。对象必须经历某种形式的构建,即使最终归结为处于未初始化状态。保留部分的内存中没有对象,无论是否初始化。如果第 5 个元素之前的空间中没有对象,那么它就不是 10 个元素的数组。
  • @αλεχολυτ 还有,请记住,在优化的构建中,编译器可能会完全忽略初始化。
  • @αλεχολυτ 你确定你想要的东西是明智的吗?你写了int a[10]; a[5] = 42;;您将如何跟踪只有 a[5] 被初始化的事实,并避免从其他索引中读取(这是未定义的行为)?
  • @GuillaumeRacicot 不幸的是,我测试的编译器似乎都无法优化不必要的初始化(请参阅下面我的答案中的示例)。即使给定一个简单的 vector&lt;int&gt; 在构造后立即被完全覆盖,每个主要编译器都会有效地发出对 memset 的不必要调用,即使在最大优化级别......
【解决方案2】:

按照您的建议使用std::unique_ptr&lt;T[]&gt; 分配内存,但要使用它- 从原始指针和元素数构造std::span(在C++20 中;gsl::span 在 C++20 之前) ,并传递跨度(按值;跨度是引用类型,有点)。跨度将为您提供容器的所有花里胡哨:大小、迭代器、范围、工作。

#include <span>
// or:
// #include <gsl/span>

int main() {

    // ... etc. ...

    {
        size_t size = 10e5;
        auto uptr { std::make_unique<double[]>(size) };
        std::span<int> my_span { uptr.get(), size };
        do_stuff_with_the_doubles(my_span);
    }

    // ... etc. ...
}

有关跨度的更多信息,请参阅:

What is a "span" and when should I use one?

【讨论】:

  • 请注意,在这种方法中,数据的所有权和有关其跨度的信息仍然分开保存……
  • 我认为它需要uptr.get() 才能编译!?除此之外,当您只需要一个本地缓冲区时,这很好。但是您也可以只将大小存储在局部变量中,使用 std:: 算法以防您需要该功能,并且非常高兴。我会假设@αλεχολυτ 正在寻找一个允许容器作为一个整体传递的解决方案!?
  • 我确实同意,一旦我们获得 C++20,使用像这样的 span 将是本地缓冲区情况的一个很好的解决方案。
  • @MichaelKenzel:自 2015 年左右以来,使用 GSL 的“核心指南支持库”,Span 已经非常有用了。无需等待 2020 年。
  • 另一种方法是制作智能指针和跨度对象的小结构,或者对智能指针和大小进行包装,以动态生成跨度。这样就可以在数组中传递。
【解决方案3】:

使用std::vector。如果您想消除更改大小的可能性,请将其包裹起来。

template <typename T>
single_allocation_vector : private std::vector<T>, public gsl::span<T>
{
    single_allocation_vector(size_t n, T t = {}) : vector(n, t), span(vector::data(), n) {}
    // other constructors to taste
};

【讨论】:

    【解决方案4】:

    对于 C++14,std::dynarrayproposed

    std::dynarray 是一个序列容器,它封装数组,其大小在构造时固定,并且在对象的整个生命周期内都不会改变。

    但有 too many issues 并没有成为标准的一部分。

    所以目前 STL 中不存在这样的容器。您可以继续使用带有initial size 的向量。

    【讨论】:

    • 我非常怀疑 OP 想要一个堆栈分配的数组。他想要一个operator[] 作用于非构造元素的向量。不过,您的答案仍然有很好的信息提及替代方案。
    • 感谢您的澄清。我认为这个问题需要具有运行时大小的std::array,但您可能是对的。我想我可以放心地留下答案,因为其他答案详细介绍了如何使用矢量。
    【解决方案5】:

    不幸的是,C++ 20 中没有添加新的容器(至少我知道没有)。但是,我同意这样的容器将非常有用。虽然只使用std::vector&lt;T&gt;reserve()emplace_back() 通常会很好,但与使用普通的new T[] 相比,它确实经常生成劣质代码,因为使用emplace_back() 似乎会抑制矢量化。如果我们改用具有初始大小的std::vector&lt;T&gt;,编译器似乎很难优化元素的值初始化,即使整个向量随后将被覆盖。 Play with an example here.

    例如,您可以使用类似的包装器

    template <typename T>
    struct default_init_wrapper
    {
        T t;
    
    public:
        default_init_wrapper() {}
        template <typename... Args>
        default_init_wrapper(Args&&... args) : t(std::forward<Args>(args)...) {}
    
        operator const T&() const { return t; }
        operator T&() { return t; }
    };
    

    std::vector<no_init_wrapper<T>> buffer(N);
    

    为了避免琐碎类型的无用初始化。这样做seems to lead to code 与普通的std::unique_ptr 版本一样好。不过我不建议这样做,因为它非常丑陋且使用起来很麻烦,因为您必须使用包装元素的向量。

    我想目前最好的选择是滚动您自己的容器。这可以作为一个起点(注意错误):

    template <typename T>
    class dynamic_array
    {
    public:
        using value_type = T;
        using reference = T&;
        using const_reference = T&;
        using pointer = T*;
        using const_pointer = const T*;
        using iterator = T*;
        using const_iterator = const T*;
        using reverse_iterator = std::reverse_iterator<iterator>;
        using const_reverse_iterator = std::reverse_iterator<const_iterator>;
        using size_type = std::size_t;
        using difference_type = std::ptrdiff_t;
    
    private:
        std::unique_ptr<T[]> elements;
        size_type num_elements = 0U;
    
        friend void swap(dynamic_array& a, dynamic_array& b)
        {
            using std::swap;
            swap(a.elements, b.elements);
            swap(a.num_elements, b.num_elements);
        }
    
        static auto alloc(size_type size)
        {
            return std::unique_ptr<T[]> { new T[size] };
        }
    
        void checkRange(size_type i) const
        {
            if (!(i < num_elements))
                throw std::out_of_range("dynamic_array index out of range");
        }
    
    public:
        const_pointer data() const { return &elements[0]; }
        pointer data() { return &elements[0]; }
    
        const_iterator begin() const { return data(); }
        iterator begin() { return data(); }
    
        const_iterator end() const { return data() + num_elements; }
        iterator end() { return data() + num_elements; }
    
        const_reverse_iterator rbegin() const { return std::make_reverse_iterator(end()); }
        reverse_iterator rbegin() { return std::make_reverse_iterator(end()); }
    
        const_reverse_iterator rend() const { return std::make_reverse_iterator(begin()); }
        reverse_iterator rend() { return std::make_reverse_iterator(begin()); }
    
        const_reference operator [](size_type i) const { return elements[i]; }
        reference operator [](size_type i) { return elements[i]; }
    
        const_reference at(size_type i) const { return checkRange(i), elements[i]; }
        reference at(size_type i) { return checkRange(i), elements[i]; }
    
        size_type size() const { return num_elements; }
    
        constexpr size_type max_size() const { return std::numeric_limits<size_type>::max(); }
    
        bool empty() const { return std::size(*this) == 0U; }
    
        dynamic_array() = default;
    
        dynamic_array(size_type size)
            : elements(alloc(size)), num_elements(size)
        {
        }
    
        dynamic_array(std::initializer_list<T> elements)
            : elements(alloc(std::size(elements))), num_elements(std::size(elements))
        {
            std::copy(std::begin(elements), std::end(elements), std::begin(*this));
        }
    
        dynamic_array(const dynamic_array& arr)
        {
            auto new_elements = alloc(std::size(arr));
            std::copy(std::begin(arr), std::end(arr), &new_elements[0]);
            elements = std::move(new_elements);
            num_elements = std::size(arr);
        }
    
        dynamic_array(dynamic_array&&) = default;
    
        dynamic_array& operator =(const dynamic_array& arr)
        {
            return *this = dynamic_array(arr);
        }
    
        dynamic_array& operator =(dynamic_array&&) = default;
    
        void swap(dynamic_array& arr)
        {
            void swap(dynamic_array& a, dynamic_array& b);
            swap(*this, arr);
        }
    
        friend bool operator ==(const dynamic_array& a, const dynamic_array& b)
        {
            return std::equal(std::begin(a), std::end(a), std::begin(b));
        }
    
        friend bool operator !=(const dynamic_array& a, const dynamic_array& b)
        {
            return !(a == b);
        }
    };
    

    【讨论】:

    • 要非常小心。您的 no init 包装器仍将初始化构造任何非平凡的类型
    • @GuillaumeRacicot 是的,我认为这是你能做的最好的事情。它至少会使琐碎的类型未初始化。对于非平凡类型,即使使用普通的 new T[N] 也必须初始化元素。在需要的情况下,包装器只是将std::vector 在代码生成方面与普通动态数组相提并论的一种方式。当然,包装器通常不是那么好……
    • 我重命名了包装器类型以更好地反映它的实际作用。
    猜你喜欢
    • 2011-02-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-07-28
    • 1970-01-01
    相关资源
    最近更新 更多