簡體   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