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

c9x0cxw0  于 2023-01-28  发布在  其他
关注(0)|答案(5)|浏览(167)

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

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

为什么std::array不能在构造函数中接收大小?(即):

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

这样,函数看起来就不那么激进,也不需要模板参数,如下所示:

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

我只想知道std::array的设计选择,以及为什么要这样设计。
这不是一个由于bug引起的问题,而是一个为什么std::array<>要以这种方式设计的问题。

tzcvj98z

tzcvj98z1#

std::array<T,N> var旨在更好地替代C样式数组T var[N]
该对象的内存空间是 * 本地 * 创建的,即在堆栈上为局部变量创建内存空间,或者在定义为成员时在结构本身内部创建内存空间。
相反,std::vector<T>总是在堆中分配其元素的内存。
因此,由于std::array是本地分配的,它不能具有可变大小,因为该空间需要在编译时保留。另一方面,std::vector具有重新分配和调整大小的能力,因为其内存是无限的。
因此,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

神箭:https://godbolt.org/z/75s47T56f

l7wslrjt

l7wslrjt2#

如果您在使用std::array时遇到问题,并且认为std::span是一个解决方案,那么现在您将遇到两个问题。
更严重的是,如果不知道func是什么样的概念操作,就很难判断什么是正确的选择。
首先,如果您希望或可以利用它在编译时知道大小,那么没有什么比您试图避免的更酷的了。

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

想象一下,在编译时知道大小可以让您编译器执行各种各样的技巧,比如完全展开循环或在编译时验证逻辑(eidogg.如果你知道大小必须小于或大于一个常数)。或者最酷的技巧,不需要为func内部的任何辅助操作分配内存(因为您事先知道问题的大小)。
如果需要动态数组,请使用(并传递)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

问问你自己,你的函数有多具体,以至于你真的真的想让它在任何大小的std::array上工作,而不是在std::vector上工作?

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

enyaitl33#

这不是一个答案,真的,因为我曾经鄙视std::array<>,原因和你一样--任何具有一元性质的东西都不是好的设计(IMNSHO)。
幸运的是,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对象。我知道MS和其他人可能在他们的库的旧版本中有一个array_view

    • TL;医生**

std::array只用于声明数组对象。用动态std::span传递它。

std::数组与C数组

std::array的用例实际上非常狭窄:将一个 * 固定大小 * 数组封装为一级容器对象(可以复制,而不仅仅是引用)。
乍一看,这似乎并没有比标准C风格数组有多大改进:

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)是指针的大小)。
1.对于std::array,参数值是调用方数据的实际 * 本地副本 *。如果希望能够修改调用方的数据,则需要明确声明形参为引用类型(myarray & a)或只是为了避免昂贵的拷贝(const myarray & a)。这与其他C对象的传递方式一致。虽然大小仍然是10,但您的代码可以使用常见的C容器习惯用法查询数组的大小:一米十八!
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>传递给函数。
我将参考这里的其他好答案,以便在处理阵列数据时考虑更多有关容器选择的问题。

f5emj3cl

f5emj3cl4#

在C++ std中有一些连续的容器和范围。它们有不同的用途。也有一些传递它们的技术。
我尽量说得详尽些。

std::array<int, 7>

这是一个7个int的缓冲区。它们存储在对象本身中。将一个array放在某个地方就是将足够的存储空间正好放置在那个位置(加上可能的对齐填充,但那是在缓冲区的末尾)。
在编译时,当您确切地知道某个东西有多大,或者需要知道时,可以使用它。

std::vector<int>

这个对象拥有一个int的缓冲区的所有权。2存储这些int的内存是动态分配的,并且可以在运行时改变。3对象本身通常有3个指针大小。4它有一些增长策略,当你每次添加一个元素时,可以避免做N^2的工作。
这个对象可以被有效地移动--如果旧对象被标记(通过std::move或其他方式)为可以安全地从其窃取状态,那么它将窃取缓冲区。

std::span<int>

这表示外部拥有的int序列,可能存储在std::array中,或由std::vector拥有,或存储在其他地方。它知道它在内存中的什么地方开始,什么时候结束。
与上面两个不同的是,它不是一个容器,而是一个范围或内容的视图,所以你不能将span分配给彼此(语义很混乱),并且你有责任确保源缓冲区持续“足够长的时间”,以至于在它消失后你不会使用它。
span经常被用作函数参数,在您的例子中,它可能解决了您的大部分问题--它允许您将不同大小的数组传递给一个函数,并且在该函数中您可以读取或写入值。
span遵循指针语义。这意味着const std::span<int>类似于int*const--* 指针 * 是const,但指向的东西不是!您可以自由修改const std::span<int>中的元素。相比之下,std::span<const int>类似于int const*--指针不是常量,但要指出的是,您可以自由更改span在std::span<const int>中引用的元素范围,但不允许修改元素本身。
最后一种技术是auto或templates,这里我们在头文件中实现函数体(或等价物),而不约束类型(或受概念约束)。

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允许您将实现从头文件中分离到cpp文件中,并且不会为不同的参数生成代码。
total0total3total4都会导致根据参数类型生成不同的代码,这可能会导致二进制膨胀问题,尤其是当代码体比所示的更复杂时,并在较大的项目中导致构建时间问题。
total1不能直接和std::array一起使用,你可以在使用代码之前把内容复制到一个动态向量中。
最后,请注意span<int>是最接近arr[], size的C方式,Span本质上是一个指向第一个指针和长度对的指针,实用程序代码将其 Package 起来。

zmeyuzjn

zmeyuzjn5#

C++11 std::array<>的主要用途是作为C风格数组[]的一个很好的替代品,特别是当它们用new声明并用delete[]解除时。
这里的主要目标是获得一个正式的托管对象,它充当数组,同时将所有可能的内容都保持为常量表达式。
正则数组的主要问题是,因为它们实际上不是对象,所以不能从它们派生类(迫使您实现迭代器),并且当您复制将它们用作对象属性的类时会很痛苦。
由于newdeletedelete[]返回指针,因此每次都需要实现一个复制构造函数,该函数将声明另一个数组,然后复制其内容,或者在该数组上维护自己的动态引用计数器。
从这个Angular 来看,std::array<>是声明纯静态数组的好方法,这些数组将由语言本身管理。

相关问题