提问人:ruff 提问时间:1/26/2023 最后编辑:DailyLearnerruff 更新时间:1/27/2023 访问量:549
为什么 std::array 需要将 size 作为模板参数而不是构造函数参数?
Why does std::array require the size as a template parameter and not as a constructor parameter?
问:
我发现这有很多设计问题,特别是在传递给函数时。基本上,当你初始化 std::array 时,它会接受两个模板参数,和 .但是,当您创建需要 和 的函数时,我们不知道大小,因此我们也需要为函数创建模板参数。std::array<>
<class T
size_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
这不是由于错误而产生的问题,而是为什么以这种方式设计的问题。std::array<>
答:
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
Godbolt:https://godbolt.org/z/75s47T56f
C++11的主要目的是成为C样式数组的体面替代品,特别是当它们被声明和用 .std::array<>
[]
new
delete[]
这里的主要目标是获得一个官方的托管对象,该对象用作数组,同时将所有可能的内容作为常量表达式进行维护。
常规数组的主要问题是,由于它们实际上不是对象,因此无法从中派生类(迫使您实现迭代器),并且当您复制将它们用作对象属性的类时会很痛苦。
由于 和 返回指针,因此每次都需要实现一个复制构造函数,该构造函数将声明另一个数组,它们复制其内容,或者在其上维护自己的动态引用计数器。new
delete
delete[]
从这个角度来看,是声明纯静态数组的好方法,这些数组将由语言本身管理。std::array<>
评论
std::array<>
std::array<>
std::array<>
真的,这不是一个答案,因为我曾经出于和你一样的原因鄙视——任何具有 Monadic 品质的东西都不是好设计 (IMNSHO)。std::array<>
幸运的是,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的;dr 仅
用于声明数组对象。用动态传递它。std::array
std::span
std::array 与 C 数组
其用例实际上非常狭窄:将固定大小的数组封装为一流的容器对象(一个可以复制的对象,而不仅仅是引用)。std::array
乍一看,这似乎比标准的 C 型数组没有太大的改进:
typedef int myarray[10]; // (1)
using myarray = std::array<int,10>; // (2)
void f( myarray a );
但事实确实如此!区别在于实际得到什么:f()
- 对于 C 样式的数组,参数只是一个指针 — 对调用方数据的引用(您可以修改!您知道引用数组 () 的大小,但是即使使用通常的 C 数组大小惯用语(,因为指针的大小),编写代码来获得该大小也不是直接的。
10
sizeof(myarray)/sizeof(a[0])
sizeof(a)
- 对于 ,参数值是调用方数据的实际本地副本。如果您希望能够修改调用方的数据,则需要明确将形式参数声明为引用类型 () 或只是为了避免昂贵的副本 ()。这与其他 C++ 对象的传递方式一致。虽然大小仍然是 ,您的代码可以使用通常的 C++ 容器习惯语查询数组的大小:!
std::array
myarray & a
const myarray & a
10
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数组,这仅仅是因为它的长度是固定的。例如,调用方不能将 a 传递给函数。std::array<int,12>
我将向你推荐此处的其他好答案,以在处理阵列数据时考虑更多关于容器选择的信息。
评论
std::span
std::span
std::array
std::array
如果你有一个问题,你认为是一个解决方案,现在你会遇到两个问题。std::array
std::span
更严重的是,在不知道什么样的概念操作的情况下,很难说出什么是正确的选择。func
首先,如果你想或可以利用在编译时知道大小,没有什么比你试图避免的更酷的了。
template<std::size_t N>
void func(std::array<int, N> arr); // add & or && or const& if appropiate
想象一下,在编译时知道大小可以让你和编译器做各种各样的技巧,比如完全展开循环或在编译时验证逻辑(例如,如果你知道大小必须小于或大于常量)。
或者最酷的技巧,不需要为内部的任何辅助操作分配内存(因为您先验地知道问题的规模)。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
问问自己,你的功能有多具体,以至于你真的想让它适用于任何大小,但不能与?
为什么特别 s 偶数?std::array
std::vector
int
template<class ArithmeticRange>
void func(ArithmeticRange dynarr); // add & or && or const& if appropiate
评论
void func(std::array<int, 3> arr);
template <size_t N>
C++ 中有一些连续的容器和范围。它们有不同的目的。还有一些技术可以传递它们。std
我会尽量做到详尽无遗。
std::array<int, 7>
这是 7 秒的缓冲区。它们存储在对象本身中。放置某个位置就是在该位置放置足够的存储空间(加上出于对齐原因的可能填充,但那是在缓冲区的末尾)。int
array
7
int
在编译时,当您确切地知道某件事有多大或需要知道时,您可以使用它。
std::vector<int>
此对象拥有 S 缓冲区的所有权。保存这些 s 的内存是动态分配的,并且可以在运行时更改。对象本身的大小通常为 3 个指针。它有一些增长策略,可以避免在你一次添加 1 个元素时做 N^2 工作。int
int
这个对象可以有效地移动 -- 如果旧对象被标记为(通过或其他方式)可以安全地窃取状态,它将窃取缓冲区。std::move
std::span<int>
这表示外部拥有的 s 序列,可能存储在 a 中或由 a 拥有,或存储在其他地方。它知道它在内存中的哪个位置开始,何时结束。int
std::array
std::vector
与上述两个不同,它不是一个容器,而是一个范围或内容视图。因此,您不能将跨度分配给彼此(语义令人困惑),并且您有责任确保源缓冲区持续“足够长的时间”,以便在它消失后不会使用它。
span
通常用作函数参数。在你的例子中,它可能解决了你的大部分问题 - 它允许你将不同大小的数组传递给一个函数,在该函数中你可以读取或写入值。
span
遵循指针语义。这意味着就像一个 -- 指针是 ,但指向的东西不是!您可以自由修改 中的元素。相比之下,就像一个 -- 指针不是 const,但指向的东西是。您可以自由更改 span 所指的元素范围,但不允许修改元素本身。const std::span<int>
int*const
const
const std::span<int>
std::span<const int>
int const*
std::span<const int>
最后一种技术是或模板。在这里,我们在标头(或等效项)中实现函数的主体,并使类型不受约束(或受概念约束)。auto
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;
}
请注意,它们都具有相同的实现。
total3
并且是相同的;您需要一个更现代的编译器来使用语法。total4
total3
total1
并允许您将实现拆分为远离头文件的 CPP 文件。此外,不会针对不同的参数进行代码生成。total2
total0
,并且都会导致根据参数的类型生成不同的代码。这可能会导致二进制膨胀问题,尤其是在主体比显示的更复杂的情况下,并导致大型项目中的构建时间问题。total3
total4
total1
不能直接与 a 一起使用。您可以在使用代码之前将内容复制到动态向量。std::array
total1({arr.begin(), arr.end()})
最后,请注意,这是最接近 C 的方式。从本质上讲,Span 是一个指向第一个和长度的指针对,并用实用程序代码包裹它。span<int>
arr[], size
评论
std::vector
params_size -> N