詳解C++語言中std::array的神奇用法
概述
std::array是在C++11標(biāo)準(zhǔn)中增加的STL容器,它的設(shè)計(jì)目的是提供與原生數(shù)組類似的功能與性能。也正因此,使得std::array有很多與其他容器不同的特殊之處,比如:std::array的元素是直接存放在實(shí)例內(nèi)部,而不是在堆上分配空間;std::array的大小必須在編譯期確定;std::array的構(gòu)造函數(shù)、析構(gòu)函數(shù)和賦值操作符都是編譯器隱式聲明的……這讓很多用慣了std::vector這類容器的程序員不習(xí)慣,覺得std::array不好用。但實(shí)際上,std::array的威力很可能被低估了。在這篇文章里,我會(huì)從各個(gè)角度介紹下std::array的用法,希望能帶來一些啟發(fā)。
本文的代碼都在C++17環(huán)境下編譯運(yùn)行。當(dāng)前主流的g++版本已經(jīng)能支持C++17標(biāo)準(zhǔn),但是很多版本(如gcc 7.3)的C++17特性不是默認(rèn)打開的,需要手工添加編譯選項(xiàng)-std=c++17。
自動(dòng)推導(dǎo)數(shù)組大小
很多項(xiàng)目中都會(huì)有類似這樣的全局?jǐn)?shù)組作為配置參數(shù):
uint32_t g_cfgPara[] = {1, 2, 5, 6, 7, 9, 3, 4};
當(dāng)程序員想要使用std::array替換原生數(shù)組時(shí),麻煩來了:
array<uint32_t, 8> g_cfgPara = {1, 2, 5, 6, 7, 9, 3, 4}; // 注意模板參數(shù)“8”
程序員不得不手工寫出數(shù)組的大小,因?yàn)樗莝td::array的模板參數(shù)之一。如果這個(gè)數(shù)組很長(zhǎng),或者經(jīng)常增刪成員,對(duì)數(shù)組大小的維護(hù)工作恐怕不是那么愉快的。有人要抱怨了:std::array的聲明用起來還沒有原生數(shù)組方便,選它干啥?
但是,這個(gè)抱怨只該限于C++17之前,C++17帶來了類模板參數(shù)推導(dǎo)特性,你不再需要手工指定類模板的參數(shù):
array g_cfgPara = {1, 2, 5, 6, 7, 9, 3, 4}; // 數(shù)組大小與成員類型自動(dòng)推導(dǎo)
看起來很美好,但很快就會(huì)有人發(fā)現(xiàn)不對(duì)頭:數(shù)組元素的類型是什么?還是std::uint32_t嗎?
有人開始嘗試只提供元素類型參數(shù),讓編譯器自動(dòng)推導(dǎo)長(zhǎng)度,遺憾的是,它不會(huì)奏效。
array<uint32_t> g_cfgPara = {1, 2, 5, 6, 7, 9, 3, 4}; // 編譯錯(cuò)誤
好吧,暫時(shí)看起來std::array是不能像原生數(shù)組那樣聲明。下面我們來解決這個(gè)問題。
用函數(shù)返回std::array
問題的解決思路是用函數(shù)模板來替代類模板——因?yàn)镃++允許函數(shù)模板的部分參數(shù)自動(dòng)推導(dǎo)——我們可以聯(lián)想到std::make_pair、std::make_tuple這類輔助函數(shù)。巧的是,C++標(biāo)準(zhǔn)真的在TS v2試驗(yàn)版本中推出過std::make_array,然而因?yàn)轭惸0鍏?shù)推導(dǎo)的問世,這個(gè)工具函數(shù)后來被刪掉了。
但顯然,用戶的需求還是存在的。于是在C++20中,又新增了一個(gè)輔助函數(shù)std::to_array。
別被C++20給嚇到了,這個(gè)函數(shù)的代碼其實(shí)很簡(jiǎn)單,我們可以把它拿過來定義在自己的C++17代碼中[1]。
template<typename R, typename P, size_t N, size_t... I> constexpr array<R, N> to_array_impl(P (&a)[N], std::index_sequence<I...>) noexcept { return { {a[I]...} }; } template<typename T, size_t N> constexpr auto to_array(T (&a)[N]) noexcept { return to_array_impl<std::remove_cv_t<T>, T, N>(a, std::make_index_sequence<N>{}); } template<typename R, typename P, size_t N, size_t... I> constexpr array<R, N> to_array_impl(P (&&a)[N], std::index_sequence<I...>) noexcept { return { {move(a[I])...} }; } template<typename T, size_t N> constexpr auto to_array(T (&&a)[N]) noexcept { return to_array_impl<std::remove_cv_t<T>, T, N>(move(a), std::make_index_sequence<N>{}); }
細(xì)心的朋友會(huì)注意到,上面這個(gè)定義與C++20的推薦實(shí)現(xiàn)有所差異,這是有目的的。稍后我會(huì)解釋這么干的原因。
現(xiàn)在讓我們嘗試下用新方法解決老問題:
auto g_cfgPara = to_array<int>({1, 2, 5, 6, 7, 9, 3, 4}); // 類型不是uint32_t?
不對(duì)啊,為什么元素類型不是原來的std::uint32_t?
這是因?yàn)槟0鍏?shù)推導(dǎo)對(duì)std::initializer_list的元素拒絕隱式轉(zhuǎn)換,如果你把to_array的模板參數(shù)從int改為uint32_t,會(huì)得到如下編譯錯(cuò)誤:
D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:51:61: error: no matching function for call to 'to_array<uint32_t>(<brace-enclosed initializer list>)' auto g_cfgPara = to_array<uint32_t>({1, 2, 5, 6, 7, 9, 3, 4}); D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:34:16: note: candidate: 'template<class T, long long unsigned int N> constexpr auto to_array(T (&)[N])' constexpr auto to_array(T (&a)[N]) noexcept ^~~~~~~~ D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:34:16: note: template argument deduction/substitution failed: D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:51:61: note: mismatched types 'unsigned int' and 'int' auto g_cfgPara = to_array<uint32_t>({1, 2, 5, 6, 7, 9, 3, 4}); D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:46:16: note: candidate: 'template<class T, long long unsigned int N> constexpr auto to_array(T (&&)[N])' constexpr auto to_array(T (&&a)[N]) noexcept ^~~~~~~~ D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:46:16: note: template argument deduction/substitution failed: D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:51:61: note: mismatched types 'unsigned int' and 'int'
Hoho,有點(diǎn)慘是不,繞了一圈回到原點(diǎn),還是不能強(qiáng)制指定類型。
這個(gè)時(shí)候,之前針對(duì)std::array做的修改派上用場(chǎng)了:我給to_array_impl增加了一個(gè)模板參數(shù),讓輸入數(shù)組的元素和返回std::array的元素用不同的類型參數(shù)表示,這樣就給類型轉(zhuǎn)換帶來了可能。為了實(shí)現(xiàn)轉(zhuǎn)換到指定的類型,我們還需要添加兩個(gè)工具函數(shù):
template<typename R, typename P, size_t N> constexpr auto to_typed_array(P (&a)[N]) noexcept { return to_array_impl<R, P, N>(a, std::make_index_sequence<N>{}); } template<typename R, typename P, size_t N> constexpr auto to_typed_array(P (&&a)[N]) noexcept { return to_array_impl<R, P, N>(move(a), std::make_index_sequence<N>{}); }
這兩個(gè)函數(shù)和to_array的區(qū)別是:它帶有3個(gè)模板參數(shù):第一個(gè)是要返回的std::array的元素類型,后兩個(gè)和to_array一樣。這樣我們就可以通過指定第一個(gè)參數(shù)來實(shí)現(xiàn)定制std::array元素類型了。
auto g_cfgPara = to_typed_array<uint32_t>({1, 2, 5, 6, 7, 9, 3, 4}); // 自動(dòng)把元素轉(zhuǎn)換成uint32_t
這段代碼可以編譯通過和運(yùn)行,但是卻有類型轉(zhuǎn)換的編譯告警。當(dāng)然,如果你膽子夠大,可以在to_array_impl函數(shù)中放一個(gè)static_cast來消除告警。但是編譯告警提示了我們一個(gè)不能忽視的問題:如果萬一輸入的數(shù)值溢出了怎么辦?
auto g_a = to_typed_array<uint8_t>({256, -1}); // 數(shù)字超出uint8_t范圍
編譯器還是一樣的會(huì)讓你編譯通過和運(yùn)行,g_a中的兩個(gè)元素的值將分別為0和255。如果你不明白為什么這兩個(gè)值和入?yún)⒉灰粯?,你該?fù)習(xí)下整型溢出與回繞的知識(shí)了。
顯然,這個(gè)方案還不完美。但我們可以繼續(xù)改進(jìn)。
編譯期字面量數(shù)值合法性校驗(yàn)
首先能想到的做法是在to_array_impl函數(shù)中放入一個(gè)if判斷之類的語句,對(duì)于超出目標(biāo)數(shù)值范圍的輸入拋出異?;蛘咦銎渌幚?。這當(dāng)然可行,但要注意的是這些工具函數(shù)是可以在運(yùn)行期調(diào)用的,對(duì)于這種常用的基礎(chǔ)函數(shù)來說,性能至關(guān)重要。一旦在里面加入了錯(cuò)誤判斷,意味著運(yùn)行時(shí)的每一次調(diào)用性能都會(huì)下降。
理想的設(shè)計(jì)是:只有在編譯期生成的數(shù)組才進(jìn)行校驗(yàn),并且報(bào)編譯錯(cuò)誤。但運(yùn)行時(shí)調(diào)用函數(shù)時(shí)不要加入任何校驗(yàn)。
可惜的是,至少在C++20之前,沒有辦法指定函數(shù)只允許在編譯期執(zhí)行[2]。那有沒有其他手段呢?
熟悉C++的人知道:C++的編譯期處理大多可以用模板的trick來完成——因?yàn)槟0鍏?shù)一定是編譯期常量。因此我們可以用模板參數(shù)來完成編譯期處理——只要把數(shù)組元素全部作為模板的非類型參數(shù)就可以了。當(dāng)然,這里有個(gè)問題:模板的非類型參數(shù)的類型怎么確定?正好C++17提供了auto模板參數(shù)的功能,可以派上用場(chǎng):
template<typename T> constexpr void CheckIntRanges() noexcept {} // 用于終結(jié)遞歸 template<typename T, auto M, auto... N> constexpr void CheckIntRanges() noexcept { // 防止無符號(hào)與有符號(hào)比較 static_assert(!((std::numeric_limits<T>::min() >= 0) && (M < 0))); // 范圍校驗(yàn) static_assert((M >= std::numeric_limits<T>::min()) && (M <= std::numeric_limits<T>::max())); CheckIntRanges<T, N...>(); } template<typename T, auto... N> constexpr auto DeclareArray() noexcept { CheckIntRanges<T, N...>(); array<T, sizeof...(N)> a{{static_cast<T>(N)...}}; return a; };
注意這個(gè)函數(shù)中,所有的校驗(yàn)都通過static_assert完成。這就保證了校驗(yàn)一定只會(huì)發(fā)生在編譯期,不會(huì)帶來任何運(yùn)行時(shí)開銷。
DeclareArray的使用方法如下:
constexpr auto a1 = DeclareArray<uint8_t, 1, 2, 3, 4, 255>(); // 聲明一個(gè)std::array<uint8_t, 5>,元素分別為1, 2, 3, 4, 255 static_assert(a1.size() == 5); static_assert(a1[3] == 4); auto a2 = DeclareArray<uint8_t, 1, 2, 3, -1>(); // 編譯錯(cuò)誤,-1超出uint8_t范圍 auto a3 = DeclareArray<uint16_t, 1, 2, 3, 65536>(); // 編譯錯(cuò)誤,65536超出uint16_t范圍
這里有一個(gè)誤區(qū)需要說明:有些人可能會(huì)把DeclareArray聲明成這樣:
template<typename T, T... N> // 注意N的類型為T constexpr auto DeclareArray() noexcept
這么做的話,會(huì)發(fā)現(xiàn)對(duì)數(shù)值的校驗(yàn)總是能通過——因?yàn)槟0鍏?shù)在進(jìn)入校驗(yàn)之前就已經(jīng)被轉(zhuǎn)換為T類型了。如果你的編譯器不支持C++17的auto模板參數(shù),那么可以通過使用std::uint64_t、std::int64_t這些“最大”的類型來間接達(dá)到目的。
另一點(diǎn)要說明的是,C++對(duì)于非類型模板參數(shù)的允許類型存在限制,DeclareArray的方法只能用于數(shù)組元素為基本類型的場(chǎng)景(至少在C++20以前如此)。但是這也足夠了。如果數(shù)組的元素是自定義類型,就可以通過自定義的構(gòu)造函數(shù)等方法來控制類型轉(zhuǎn)換。
如果你看到這里覺得有點(diǎn)意思了,那就對(duì)了,后面還有更過癮的。
編譯期生成數(shù)組
C++11中新增的constexpr修飾符可以在編譯期完成很多計(jì)算工作。但是一般constexpr函數(shù)只能返回單個(gè)值,一旦你想用它返回一串對(duì)象的集合,就會(huì)遇到麻煩:STL容器都有動(dòng)態(tài)內(nèi)存申請(qǐng)功能,不能作為編譯期常量(至少在C++20之前如此);而原生數(shù)組作為返回值會(huì)退化為指針,導(dǎo)致返回懸空的指針。即使是返回?cái)?shù)組的引用也是不行的,會(huì)產(chǎn)生懸空的引用。
constexpr int* Func() noexcept { int a[] = {1, 2, 3, 4}; return a; // 嚴(yán)重錯(cuò)誤!返回局部對(duì)象的地址 }
直到std::array的出現(xiàn),這個(gè)問題才得到較好解決。std::array既可以作為編譯期常量,又可以作為函數(shù)返回值。于是,它成為了編譯期返回集合數(shù)據(jù)的首選。
在上面to_array等工具函數(shù)的實(shí)現(xiàn)中,我們已經(jīng)見過了編譯期返回?cái)?shù)組是怎么做的。這里我們?cè)俅竽懸稽c(diǎn),寫一個(gè)編譯期冒泡排序:
template<typename T, size_t N> constexpr std::array<T, N> Sort(const std::array<T, N>& numbers) noexcept { std::array<T, N> sorted(numbers); for (int i = 0; i < N; ++i) { for (int j = N - 1; j > i; --j) { if (sorted[j] < sorted[j - 1]) { T t = sorted[j]; sorted[j] = sorted[j - 1]; sorted[j - 1] = t; } } } return sorted; } int main() { constexpr std::array<int, 4> before{4, 2, 3, 1}; constexpr std::array<int, 4> after = Sort(before); static_assert(after[0] == 1); static_assert(after[1] == 2); static_assert(after[2] == 3); static_assert(after[3] == 4); return 0; }
因?yàn)檎麄€(gè)排序算法都是在編譯期完成,所以我們沒有必要太關(guān)注冒泡排序的效率問題。當(dāng)然,只要你愿意,完全可以寫出一個(gè)編譯期快速排序——畢竟constexpr函數(shù)也可以在運(yùn)行期使用,不好說會(huì)不會(huì)有哪個(gè)憨憨在運(yùn)行時(shí)調(diào)用它。
在編寫constexpr函數(shù)時(shí),有兩點(diǎn)需要注意:
1.constexpr函數(shù)中不能調(diào)用非constexpr函數(shù)。因此在交換元素時(shí)不能用std::swap,排序也不能直接調(diào)用std::sort。
2. 傳入的數(shù)組是constexpr的,因此參數(shù)類型必須加上const,也不能對(duì)數(shù)據(jù)進(jìn)行就地排序,必須返回一個(gè)新的數(shù)組。
雖然限制很多,但編譯期算法的好處也是巨大的:如果運(yùn)算中有數(shù)組越界等未定義行為,編譯將會(huì)失敗。相比起運(yùn)行時(shí)的測(cè)試,編譯期測(cè)試constexpr函數(shù)能有效的提前攔截問題。而且只要編譯通過就意味著測(cè)試通過,比起額外跑白盒測(cè)試用例方便多了。
上面的一大串static_assert語句讓人看了不舒服。這么寫的原因是std::array的operator==函數(shù)并非constexpr(至少在C++20前如此)。但是我們也可以自己定義一個(gè)模板函數(shù)用于判斷兩個(gè)數(shù)組是否相等:
template<typename T, typename U, size_t M, size_t N> constexpr bool EqualsImpl(const T& lhs, const U& rhs) { static_assert(M == N); for (size_t i = 0; i < M; ++i) { if (lhs[i] != rhs[i]) { return false; } } return true; } template<typename T, typename U> constexpr bool Equals(const T& lhs, const U& rhs) { return EqualsImpl<T, U, size(lhs), size(rhs)>(lhs, rhs); } template<typename T, typename U, size_t N> constexpr bool Equals(const T& lhs, const U (&rhs)[N]) { return EqualsImpl<T, const U (&)[N], size(lhs), N>(lhs, rhs); } int main() { constexpr std::array<int, 4> before{4, 2, 3, 1}; constexpr std::array<int, 4> after = Sort(before); static_assert(Equals(after, {1, 2, 3, 4})); // 比較std::array和原生數(shù)組 static_assert(!Equals(before, after)); // 比較兩個(gè)std::array return 0; }
我們定義的Equals比std::array的比較運(yùn)算符更強(qiáng)大,甚至可以在std::array和原生數(shù)組之間進(jìn)行比較。
對(duì)于Equals有兩點(diǎn)需要說明:
1.std::size是C++17提供的工具函數(shù),對(duì)各種容器和數(shù)組都能返回其大小。當(dāng)然,這里的Equals只會(huì)允許編譯期確定大小的容器傳入,否則觸發(fā)編譯失敗。
2.Equals定義了兩個(gè)版本,這是被C++的一個(gè)限制所逼的迫不得已:C++禁止{...}這種std::initializer_list字面量被推導(dǎo)為模板參數(shù)類型,因此我們必須提供一個(gè)版本聲明參數(shù)類型為數(shù)組,以便{1, 2, 3, 4}這種表達(dá)式能作為參數(shù)傳進(jìn)去。
編譯期排序是一個(gè)啟發(fā)性的嘗試,我們可以用類似的方法生成其他的編譯期集合常量,比如指定長(zhǎng)度的自然數(shù)序列:
template<typename T, size_t N> constexpr auto NaturalNumbers() noexcept { array<T, N> arr{0}; // 顯式初始化不能省 for (size_t i = 0; i < N; ++i) { arr[i] = i + 1; } return arr; } int main() { constexpr auto arr = NaturalNumbers<uint32_t, 5>(); static_assert(Equals(arr, {1, 2, 3, 4, 5})); return 0; }
這段代碼的編譯運(yùn)行都沒有問題,但它并不是推薦的做法。原因是在NaturalNumbers函數(shù)中,先定義了一個(gè)內(nèi)容全0的局部數(shù)組,然后再挨個(gè)修改它的值,這樣沒有直接返回指定值的數(shù)組效率高。有人會(huì)想能不能把a(bǔ)rr的初始化給去掉,但這樣會(huì)導(dǎo)致編譯錯(cuò)誤——constexpr函數(shù)中不允許定義沒有初始化的局部變量。
可能有人覺得這些計(jì)算都是編譯期完成的,對(duì)運(yùn)行效率沒影響——但是不要忘了constexpr函數(shù)也可以在運(yùn)行時(shí)調(diào)用。更好的做法可以參見前面to_array函數(shù)的實(shí)現(xiàn),讓數(shù)組的初始化一氣呵成,省掉挨個(gè)賦值的步驟。
我們用這個(gè)新思路,寫一個(gè)通用的數(shù)組生成器,它可以接受一個(gè)函數(shù)對(duì)象作為參數(shù),通過調(diào)用這個(gè)函數(shù)對(duì)象來生成數(shù)組每個(gè)元素的值。下面的代碼還演示了下如何用這個(gè)生成器在編譯期生成奇數(shù)序列和斐波那契數(shù)列。
template<typename T> constexpr T OddNumber(size_t i) noexcept { return i * 2 + 1; } template<typename T> constexpr T Fibonacci(size_t i) noexcept { if (i <= 1) { return 1; } return Fibonacci<T>(i - 1) + Fibonacci<T>(i - 2); } template<typename T, size_t N, typename F, size_t... I> constexpr array<std::remove_cv_t<T>, N> GenerateArrayImpl(F f, std::index_sequence<I...>) noexcept { return { {f(I)...} }; } template<size_t N, typename F, typename T = invoke_result_t<F, size_t>> constexpr array<T, N> GenerateArray(F f) noexcept { return GenerateArrayImpl<T, N>(f, std::make_index_sequence<N>{}); } int main() { constexpr auto oddNumbers = GenerateArray<5>(OddNumber<uint8_t>); static_assert(Equals(oddNumbers, {1, 3, 5, 7, 9})); constexpr auto fiboNumbers = GenerateArray<5>(Fibonacci<uint32_t>); static_assert(Equals(fiboNumbers, {1, 1, 2, 3, 5})); // 甚至可以傳入lambda來定制要生成的數(shù)字序列(限定C++17) constexpr auto specified = GenerateArray<3>([](size_t i) { return i + 10; }); static_assert(Equals(specified, {10, 11, 12})); return 0; }
最后那個(gè)傳入lambda來定制數(shù)組的做法存在一個(gè)疑問:lambda是constexpr函數(shù)嗎?答案為:可以是,但需要C++17支持。
GenerateArray這個(gè)數(shù)組生成器將會(huì)在后面發(fā)揮重大作用,繼續(xù)往下看。
截取子數(shù)組
std::array并未提供輸入一個(gè)指定區(qū)間來建立新容器的構(gòu)造函數(shù),但是借助上面的數(shù)組生成器,我們可以寫個(gè)輔助函數(shù)來實(shí)現(xiàn)子數(shù)組生成操作(這里再次用上了lambda函數(shù)作為生成算法)。
template<size_t N, typename T> constexpr auto SubArray(T&& t, size_t base) noexcept { return GenerateArray<N>([base, t = forward<T>(t)](size_t i) { return t[base + i]; }); } template<size_t N, typename T, size_t M> constexpr auto SubArray(const T (&t)[M], size_t base) noexcept { return GenerateArray<N>([base, &t](size_t i) { return t[base + i]; }); } int main() { // 以std::initializer_list字面量為原始數(shù)據(jù) constexpr auto x = SubArray<3>({1, 2, 3, 4, 5, 6}, 2); // 下標(biāo)為2開始,取3個(gè)元素 static_assert(Equals(x, {3, 4, 5})); // 以std::array為原始數(shù)據(jù) constexpr auto x1 = SubArray<2>(x, 1); // 下標(biāo)為1開始,取2個(gè)元素 static_assert(Equals(x1, {4, 5})); // 以原生數(shù)組為原始數(shù)據(jù) constexpr uint8_t a[] = {9, 8, 7, 6, 5}; constexpr auto y = SubArray<2>(a, 3); static_assert(Equals(y, {6, 5})); // 下標(biāo)為3開始,取2個(gè)元素 // 以字符串為原始數(shù)據(jù),注意生成的數(shù)組不會(huì)自動(dòng)加上'\0' constexpr const char* str = "Hello world!"; constexpr auto z = SubArray<5>(str, 6); static_assert(Equals(z, {'w', 'o', 'r', 'l', 'd'})); // 下標(biāo)為6開始,取5個(gè)元素 // 以std::vector為原始數(shù)據(jù),非編譯期計(jì)算 vector<int32_t> v{10, 11, 12, 13, 14}; size_t n = 2; auto d = SubArray<3>(v, n); // 運(yùn)行時(shí)生成數(shù)組 assert(Equals(d, {12, 13, 14})); // 注意不能用static_assert,不是編譯期常量 return 0; }
使用SubArray時(shí),模板參數(shù)N是要截取的子數(shù)組大小,入?yún)是任意能支持下標(biāo)操作的類型,入?yún)ase是截取元素的起始位置。由于std::array的大小在編譯期是確定的,因此N必須是編譯期常量,但參數(shù)base可以是運(yùn)行時(shí)變量。
當(dāng)所有入?yún)⒍际蔷幾g期常量時(shí),生成的子數(shù)組也是編譯期常量。
SubArray提供了兩個(gè)版本,目的也是為了讓std::initializer_list字面量可以作為參數(shù)傳入。
拼接多個(gè)數(shù)組
采用類似的方式可以做多個(gè)數(shù)組的拼接,這里同樣用了lambda作為生成函數(shù)。
template<typename T> constexpr auto TotalLength(const T& arr) noexcept { return size(arr); } template<typename P, typename... T> constexpr auto TotalLength(const P& p, const T&... arr) noexcept { return size(p) + TotalLength(arr...); } template<typename T> constexpr auto PickElement(size_t i, const T& arr) noexcept { return arr[i]; } template<typename P, typename... T> constexpr auto PickElement(size_t i, const P& p, const T&... arr) noexcept { if (i < size(p)) { return p[i]; } return PickElement(i - size(p), arr...); } template<typename... T> constexpr auto ConcatArrays(const T&... arr) noexcept { return GenerateArray<TotalLength(arr...)>([&arr...](size_t i) { return PickElement(i, arr...); }); } int main() { constexpr int32_t a[] = {1, 2, 3}; // 原生數(shù)組 constexpr auto b = to_typed_array<int32_t>({4, 5, 6}); // std::array constexpr auto c = DeclareArray<int32_t, 7, 8>(); // std::array constexpr auto x = ConcatArrays(a, b, c); // 把3個(gè)數(shù)組拼接在一起 static_assert(Equals(x, {1, 2, 3, 4, 5, 6, 7, 8})); return 0; }
和之前一樣,ConcatArrays使用了模板參數(shù)來同時(shí)兼容原生數(shù)組和std::array,它甚至可以接受任何編譯期確定長(zhǎng)度的自定義類型參與拼接。
ConcatArrays函數(shù)因?yàn)榭勺儏?shù)的語法限制,沒有再對(duì)std::initializer_list字面量進(jìn)行適配,這導(dǎo)致std::initializer_list字面量不能再直接作為參數(shù):
constexpr auto x = ConcatArrays(a, {4, 5, 6}); // 編譯錯(cuò)誤
但是我們有辦法規(guī)避這個(gè)問題:利用前面介紹過的工具把std::initializer_list先轉(zhuǎn)成std::array就可以了:
constexpr auto x = ConcatArrays(a, to_array({4, 5, 6})); // OK
編譯期拼接字符串
std::array適合用來表示字符串么?回答這個(gè)問題前,我們先看看原生數(shù)組是否適合表示字符串:
char str[] = "abc"; // str數(shù)組大小為4,包括結(jié)尾的'\0'
上面是很常見的寫法。由于數(shù)組名可退化為指針,str可用于各種需要字符串的場(chǎng)合,如傳給cout打印輸出。
std::array作為對(duì)原生數(shù)組的替代,自然也適合用來表示字符串。有人可能會(huì)覺得std::array沒法直接作為字符串類型使用,不太方便。但實(shí)際上只要調(diào)用data方法,std::array就會(huì)返回能作為字符串使用的指針:
constexpr auto str = to_array("abc"); // to_array可以將字符串轉(zhuǎn)換為std::array static_assert(str.size() == 4); static_assert(Equals(str, "abc")); // Equals也可以接受字符串字面量 cout << str.data(); // 打印字符串內(nèi)容
由于字符串字面量是char[]類型,因此前面所編寫的工具函數(shù),都可以將字符串作為輸入?yún)?shù)。上面的Equals只是其中一個(gè)例子。
那之前寫的數(shù)組拼接函數(shù)ConcatArrays能用于拼接字符串么?能,但結(jié)果和我們想的有差異:
constexpr auto str = ConcatArrays("abc", "def"); static_assert(str.size() == 8); // 長(zhǎng)度不是7? static_assert(Equals(str, {'a', 'b', 'c', '\0', 'd', 'e', 'f', '\0'}));
因?yàn)槊總€(gè)字符串結(jié)尾都有'\0'結(jié)束符,用數(shù)組拼接方法把它們拼到一起時(shí),中間的'\0'沒有被去掉,導(dǎo)致結(jié)果字符串被切割為了多個(gè)C字符串。
這個(gè)問題解決起來也很容易,只要在拼接數(shù)組時(shí)把所有數(shù)組的最后一個(gè)元素('\0')去掉,并且在返回?cái)?shù)組的末尾加上'\0'就可以了。下面的代碼實(shí)現(xiàn)了字符串拼接功能,非類型參數(shù)E是字符串的結(jié)束符,通常為'\0',但是也允許定制。我們甚至可以利用它來拼接結(jié)束符為其他值的對(duì)象,比如消息、報(bào)文等。
// 最后一個(gè)字符,放入結(jié)束符 template<auto E> constexpr auto PickChar(size_t i) { return E; } template<auto E, typename P, typename... T> constexpr auto PickChar(size_t i, const P& p, const T&... arr) { if (i < (size(p) - 1)) { if (p[i] == E) { // 結(jié)束符不允許出現(xiàn)在字符串中間 throw "terminator in the middle"; } return p[i]; } if (p[size(p) - 1] != E) { // 結(jié)束符必須是最后一個(gè)字符 throw "terminator not at end"; } return PickChar<E>(i - (size(p) - 1), arr...); } template<typename... T, auto E = '\0'> constexpr auto ConcatStrings(const T&... str) { return GenerateArray<TotalLength(str...) - sizeof...(T) + 1>([&str...](size_t i) { return PickChar<E>(i, str...); }); } int main() { constexpr char a[] = "I "; // 原生數(shù)組形式的字符串 constexpr auto b = to_array("love "); // std::array形式的字符串 constexpr auto str = ConcatStrings(a, b, "C++"); // 拼接 數(shù)組 + std::array + 字符串字面量 static_assert(Equals(str, "I love C++")); return 0; }
這段代碼中用了兩個(gè)throw,這是為了校驗(yàn)輸入的參數(shù)是否都為合法的字符串,即:字符串長(zhǎng)度=容器長(zhǎng)度-1。如果不符合該條件,會(huì)導(dǎo)致拼接結(jié)果的長(zhǎng)度計(jì)算錯(cuò)誤。
當(dāng)編譯期的計(jì)算拋出異常時(shí),只會(huì)出現(xiàn)編譯錯(cuò)誤,因此只要不在運(yùn)行時(shí)調(diào)用ConcatStrings,這兩個(gè)throw語句不會(huì)有更多影響。但因?yàn)檫@個(gè)校驗(yàn)的存在,強(qiáng)烈不建議在運(yùn)行期調(diào)用ConcatStrings做拼接,何況運(yùn)行期也沒必要用這種方法——std::string的加法操作它不香么?
有人會(huì)想:能否在編譯期計(jì)算字符串的實(shí)際長(zhǎng)度,而不是用容器的長(zhǎng)度呢?這個(gè)方法看似可行,定義一個(gè)編譯期計(jì)算字符串長(zhǎng)度的函數(shù)確實(shí)很容易:
template<typename T, auto E = '\0'> constexpr size_t StrLen(const T& str) noexcept { size_t i = 0; while (str[i] != E) { ++i; } return i; } constexpr const char* g_str = "abc"; int main() { // 利用StrLen把一個(gè)字符串按實(shí)際長(zhǎng)度轉(zhuǎn)成std::array constexpr auto str = SubArray<StrLen(g_str) + 1>(g_str, 0); static_assert(Equals(str, "abc")); return 0; }
但是,一旦你試圖把StrLen放到ConcatStrings的內(nèi)部去聲明數(shù)組長(zhǎng)度,就會(huì)產(chǎn)生問題:C++的constexpr機(jī)制要求只有在能看到輸入?yún)?shù)的constexpr屬性的地方,才允許StrLen的返回結(jié)果確定為constexpr。而在函數(shù)內(nèi)部時(shí),看到的參數(shù)類型并不是constexpr。
當(dāng)然我們可以變通一下,做出一些有趣的工具,比如使用萬惡的宏:
// 把一個(gè)字符串按實(shí)際長(zhǎng)度轉(zhuǎn)成std::array #define StrToArray(x) SubArray<StrLen(x) + 1>(x, 0) constexpr const char* g_str = "abc"; int main() { // 使用宏,可以讓constexpr指針類型也參與編譯期字符串的拼接 constexpr auto str = ConcatStrings(StrToArray(g_str), "def"); static_assert(Equals(str, "abcdef")); return 0; }
使用宏以后,ConcatStrings連編譯期不確定大小的指針類型都可以間接作為輸入了[3]。如果你狠得下心使用變參宏,甚至可以定義出按實(shí)際字符串長(zhǎng)度計(jì)算結(jié)果數(shù)組長(zhǎng)度的更通用拼接函數(shù)。但我嚴(yán)重懷疑這種需求的必要性——畢竟我們只是做編譯期的拼接,而編譯期的字符串不應(yīng)該會(huì)有結(jié)束符位置不在末尾的場(chǎng)景。
看到這里的人,或多或少該佩服一下std::array的強(qiáng)大了。上面這些編譯期操作,用原生數(shù)組很難完成吧?
展望C++20——打破更多的枷鎖
我在文章中說了多少次“至少在C++20之前如此”?不記得了,但是能確定的是:C++20會(huì)帶來很多美好的東西:std::array會(huì)有constexpr版本的比較運(yùn)算符;函數(shù)可以用consteval限定只在編譯期調(diào)用;模板非類型參數(shù)允許更多的類型;STL容器對(duì)象可以作為constexpr常量……所有這一切,都只是C++20的minor更新而已,在絕大多數(shù)的特性介紹中,它們連提都不會(huì)被提到!
可想而知,用上C++20以后,編程會(huì)發(fā)生多大的變化。那時(shí)我們?cè)賮碚艺腋嘤腥さ挠梅?/p>
尾注
[1]to_array定義了兩個(gè)版本,分別以左值引用和右值引用作為參數(shù)類型。按照C++11的最優(yōu)實(shí)踐,這樣的函數(shù)本應(yīng)該只定義一個(gè)版本并且使用完美轉(zhuǎn)發(fā)。但是to_array的場(chǎng)景如果用萬能引用會(huì)帶來一個(gè)問題:C++禁止std::initializer_list字面量{...}被推導(dǎo)為模板類型參數(shù),完美轉(zhuǎn)發(fā)方案會(huì)導(dǎo)致std::initializer_list字面量不能作為to_array的入?yún)ⅰT诤竺鎯?nèi)容中我們會(huì)看到多次這個(gè)限制所帶來的影響。
[2]C++20加入了consteval修飾符,可以指定函數(shù)只允許在編譯期調(diào)用。
[3] 需要注意的是:constexpr用于修飾指針時(shí),表示的是指針本身為常量(而不是其指向的對(duì)象)。和const不同,constexpr并不允許放在類型聲明表達(dá)式的中間。因此如果要在編譯期計(jì)算一個(gè)constexpr指針指向的字符串長(zhǎng)度,這個(gè)字符串必須位于靜態(tài)數(shù)據(jù)區(qū)里,不能位于?;蛘叨焉希ǚ駝t其地址無法在編譯期確定)。
以上就是詳解C++語言中std::array的神奇用法的詳細(xì)內(nèi)容,更多關(guān)于C++語言中std::array的神奇用法的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C/C++?Qt?TreeWidget?單層樹形組件應(yīng)用小結(jié)
TreeWidget?目錄樹組件,該組件適用于創(chuàng)建和管理目錄樹結(jié)構(gòu),在開發(fā)中我們經(jīng)常會(huì)把它當(dāng)作一個(gè)升級(jí)版的ListView組件使用,本文將通過TreeWidget實(shí)現(xiàn)多字段顯示,并增加一個(gè)自定義菜單,通過在指定記錄上右鍵可彈出該菜單并對(duì)指定記錄進(jìn)行操作2021-11-11C++ 虛函數(shù)的詳解及簡(jiǎn)單實(shí)例
這篇文章主要介紹了C++ 虛函數(shù)的詳解及簡(jiǎn)單實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-06-06Qt使用隨機(jī)驗(yàn)證碼的實(shí)現(xiàn)示例
有時(shí)候在登錄界面需要驗(yàn)證碼功能,這樣能夠防止被惡意程序攻擊,本文主要介紹了Qt使用隨機(jī)驗(yàn)證碼的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下2024-01-01