繁体   English   中英

为什么 std::array 需要大小作为模板参数而不是构造函数参数?

[英]Why does std::array require the size as a template parameter and not as a constructor parameter?

我发现了很多设计问题,尤其是将std::array<>传递给函数时。 基本上,当您初始化 std::array 时,它接受两个模板参数, <class Tsize_t size> 但是,当您创建需要std::array的 function 时,我们不知道大小,因此我们还需要为函数创建模板参数。

template <size_t params_size> auto func(std::array<int, params_size> arr);

为什么std::array不能在构造函数中取而代之? (IE):

auto array = std::array<int>(10);

然后函数看起来不那么激进并且不需要模板参数,如下所示:

auto func (std::array<int> arr);

我只想知道std::array的设计选择,以及为什么这样设计。

这不是由于错误引起的问题,而是为什么std::array<>以这种方式设计的问题。

std::array<T,N> var旨在更好地替代 C 风格的 arrays T var[N]

这个 object 的 memory 空间是在本地创建的,即在局部变量的堆栈上或在定义为成员时在结构本身内部。

相反, std::vector<T>总是在堆中分配它的元素 memory。

因此,由于std::array是在本地分配的,因此它不能具有可变大小,因为该空间需要在编译时保留。 另一方面, std::vector具有重新分配和调整大小的能力,因为它的 memory 是无界的。

因此, std::array在性能方面的一大优势在于它消除了std::vector为其灵活性付出的间接级别。

例如:

#include <cstdint>
#include <iostream>
#include <vector>
#include <array>

int main() {
    int a;
    char b[10];
    std::vector<char> c(10);
    std::array<char,10> d;
    struct E {
        std::array<char,10> e1;
        std::vector<char> e2{10};
    };
    E e;

    printf( "Stack address:   %p\n", __builtin_frame_address(0));
    printf( "Address of a:    %p\n", &a );
    printf( "Address of b:    %p\n", b );
    printf( "Address of b[0]: %p\n", &b[0] );
    printf( "Address of c:    %p\n", &c );
    printf( "Address of c[0]: %p\n", &c[0] );
    printf( "Address of d:    %p\n", &d );
    printf( "Address of d[0]: %p\n", &d[0] );
    printf( "Address of e:    %p\n", &e );
    printf( "Address of e1:   %p\n", &e.e1 );
    printf( "Address of e1[0]:%p\n", &e.e1[0] );
    printf( "Address of e2:   %p\n", &e.e2);
    printf( "Address of e2[0]:%p\n", &e.e2[0] );
}

产品

Program stdout
Stack address:   0x7fffeb115ed0
Address of a:    0x7fffeb115eb0
Address of b:    0x7fffeb115ea6
Address of b[0]: 0x7fffeb115ea6
Address of c:    0x7fffeb115e80
Address of c[0]: 0x1cad2b0
Address of d:    0x7fffeb115e76
Address of d[0]: 0x7fffeb115e76
Address of e:    0x7fffeb115e40
Address of e1:   0x7fffeb115e40
Address of e1[0]:0x7fffeb115e40
Address of e2:   0x7fffeb115e50
Address of e2[0]:0x1cad2d0

Godbolt: https://godbolt.org/z/75s47T56f

不是答案,真的,因为我曾经出于与您相同的原因而鄙视std::array<> — 任何具有 Monadic 品质的东西都不是好的设计(恕我直言)。

幸运的是,C++20 有解决方案:动态std::span<>

#include <array>
#include <iostream>
#include <span>

namespace detail
{
  void print( const std::span<const int> & xs )
  {
    for (size_t n = 0;  n < xs.size();  n++)
      std::cout << xs[n] << " ";
  }
}

void print( const std::span<const int> & xs )
{
  std::cout << "{ ";
  detail::print( xs );
  std::cout << "}\n";
}

void add( const std::span<int> & xs, int n )
{
  for (int & x : xs)
    x += n;
}

int main()
{
  std::array<int,5> xs { 1, 2, 4, 6, 10 };
  add( xs, 1 );
  print( xs );
}

请注意, span本身在所有情况下都是const ,但元素本身是可修改的,除非它们也被标记为const 这正是数组的样子。

std::span是 C++20 object。我知道 MS 和其他人可能在他们的库的旧版本中有一个array_view

tl;博士
仅使用std::array来声明您的数组 object。使用动态std::span传递它。


std::array 与 C 数组

std::array的用例实际上非常狭窄:封装一个固定大小的数组作为一级容器 object(可以复制,而不仅仅是引用)。

乍一看,这似乎与标准 C 风格 arrays 相比没有太大改进:

typedef int myarray[10];             // (1)
using myarray = std::array<int,10>;  // (2)

void f( myarray a );

但它是! 不同之处在于f()实际得到的是什么:

  1. 对于 C 风格的数组,参数只是一个指针——对调用者数据(您可以修改)的引用。 您知道引用数组的大小 ( 10 ),但是即使使用通常的 C 数组大小惯用语 ( sizeof(myarray)/sizeof(a[0]) ,编写代码来获取该大小也不是直截了当的,因为sizeof(a)是指针的大小)。
  2. 对于std::array ,参数值是调用者数据的实际本地副本 如果您希望能够修改调用者的数据,那么您需要明确将形式参数声明为引用类型 ( myarray & a ) 或只是为了避免昂贵的副本 ( const myarray & a )。 这与其他 C++ 对象的传递方式一致。 虽然大小仍然是10 ,但您的代码可以使用通常的 C++ 容器惯用语查询数组的大小: a.size()

C 克服这个问题的通常方法是用有关数组大小的信息使调用站点和正式参数列表混乱,这样它就不会丢失。

int f( int array[], size_t n )   // traditional C
{
  printf( "There are %zu elements.\n", n );
  recurse with f( array, n );
}

int main(void)
{
  int my_array[10];
  f( my_array, ARRAY_SIZE(my_array) );

std::array方式更干净。

int f( std::array<int,10> & array )   // C++
{
  std::cout << "There are " << array.size() << " elements.\n";
  recurse with f( array );
}

int main()
{
  std::array<int,10> my_array;
  f( my_array );

但是虽然更简洁,但它的灵活性明显不如 C 数组,原因很简单,因为它的长度是固定的。 例如,调用者不能将std::array<int,12>传递给 function。

我将在此处向您推荐其他好的答案,以便在处理阵列数据时更多地考虑容器选择。

如果您对std::array有疑问并且您认为std::span是一个解决方案,那么现在您将遇到两个问题。

更严重的是,如果不知道什么样的概念操作是有效的,就很难func什么是正确的选择。

首先,如果你想或可以利用在编译时知道大小,没有什么比你试图避免的更酷了。

template<std::size_t N> 
void func(std::array<int, N> arr);   // add & or && or const& if appropiate

想象一下,在编译时知道大小可以让您编译器执行各种技巧,例如完全展开循环或在编译时验证逻辑(例如,如果您知道大小必须小于或大于常量)。 或者最酷的技巧,不需要为func内的任何辅助操作分配 memory(因为你先验地知道问题的大小)。

如果你想要一个动态数组,使用(并传递)一个std::vector

void func(std::vector<int> dynarr);   // add & or && or const& if appropiate

但是随后您强制调用者使用std::vector作为容器。

如果你想要一个固定的数组,它适用于所有东西,

template<class FixedArray>
void func(FixedArray dynarr);   // add & or && or const& if appropiate

问问自己,您的 function 有多具体,以至于您真的想让它与任何大小的std::array一起工作,但不能与std::vector一起工作? 为什么特别是int s even?

template<class ArithmeticRange>
void func(ArithmeticRange dynarr);   // add & or && or const& if appropiate

C++ std中有一些连续的容器和范围。 它们有不同的用途。 还有一些技术可以传递它们。

我会尽量详尽无遗。

std::array<int, 7>

这是一个 7 int的缓冲区。 它们存储在 object 本身中。 将一个array放在某处就是在该位置为恰好7 int放置足够的存储空间(加上可能出于 alignment 原因的填充,但那是在缓冲区的末尾)。

当您在编译时确切知道某物有多大或需要知道时,您可以使用它。

std::vector<int>

这个 object 拥有一个int缓冲区的所有权。 保存这些int的 memory 是动态分配的,可以在运行时更改。 object 本身的大小通常是 3 个指针。 它有一些增长策略,可以避免在您一次向其添加 1 个元素时进行 N^2 工作。

这个 object 可以有效地移动——如果旧的 object 被标记(通过std::move或其他方式)可以安全地从中窃取 state,它将窃取缓冲区。

std::span<int>

这表示外部拥有的int序列,可能存储在std::array中或由std::vector拥有,或存储在其他地方。 它知道它在 memory 中的何处开始以及何时结束。

与上面两个不同的是,它不是容器,而是内容的范围或视图。 所以你不能互相分配跨度(语义混乱),你有责任确保源缓冲区持续“足够长”,以至于你在它消失后不再使用它。

span通常用作 function 参数。 在你的情况下,它可能解决了你的大部分问题 - 它允许你将不同大小的 arrays 传递给 function,并且在该 function 中你可以读取或写入值。

span遵循指针语义。 这意味着const std::span<int>就像一个int*const ——指针const ,但指向的东西不是! 您可以自由修改const std::span<int>中的元素。 相比之下, std::span<const int>就像一个int const* ——指针不是 const,但指向的东西是。 您可以自由更改 span 在std::span<const int>中引用的元素范围,但您不能修改元素本身。

最后一种技术是auto或模板。 在这里,我们在 header(或等效项)中实现 function 的主体,并使类型不受约束(或受概念约束)。

template<std::size_t N>
int total0( std::array<int, N> const& elems ) {
  int r = 0;
  for (int e:elems) r+=e;
  return r;
}

int total1( std::vector<int> const& elems ) {
  int r = 0;
  for (int e:elems) r+=e;
  return r;
}

int total2( std::span<int const> elems ) {
  int r = 0;
  for (int e:elems) r+=e;
  return r;
}

int total3( auto const& elems ) {
  int r = 0;
  for (int e:elems) r+=e;
  return r;
}

template<class Ints>
int total4( Ints const& elems ) {
  int r = 0;
  for (int e:elems) r+=e;
  return r;
}

注意这些都有相同的实现。

total3total4相同; 你需要一个更现代的编译器来使用total3语法。

total1total2允许您将实现从 header 文件拆分到一个 cpp 文件中。 此外,不会为不同的 arguments 生成代码。

total0total3total4都会导致根据 arguments 的类型生成不同的代码。这可能会导致二进制膨胀问题,尤其是当主体比显示的更复杂时,并且会导致大型项目中的构建时间问题。

total1不能直接与std::array一起使用。 您可以执行total1({arr.begin(), arr.end()}) ,这会在使用代码之前将内容复制到动态向量中。

最后,请注意span<int>是最接近arr[], size的 C 方式的。 Span 本质上是指向第一个指针和长度的指针对,实用程序代码将其包装起来。

C++11 std::array<>的主要目的是成为 C 风格 arrays []的一个不错的替代品,尤其是当它们用new声明并用delete[]消除时。

这里的主要目标是获得一个官方的、托管的 object 作为数组,同时将所有可能的内容维护为常量表达式。

常规 arrays 的主要问题是,由于它们实际上不是对象,因此无法从它们派生出 class(迫使您实现迭代器),并且在复制将它们用作 object 属性的类时会很痛苦。

由于newdeletedelete[]返回指针,您每次都需要实现一个复制构造函数,该构造函数将声明另一个数组并复制其内容,或者在其上维护您自己的动态引用计数器。

从这个角度来看, std::array<>是声明纯 static arrays 的好方法,它将由语言本身管理。

暂无
暂无

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

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