C++語言中std::array的神奇用法總結,你須要知道!

摘要:在這篇文章裏,將從各個角度介紹下std::array的用法,但願能帶來一些啓發。

td::array是在C++11標準中增長的STL容器,它的設計目的是提供與原生數組相似的功能與性能。也正所以,使得std::array有不少與其餘容器不一樣的特殊之處,好比:std::array的元素是直接存放在實例內部,而不是在堆上分配空間;std::array的大小必須在編譯期肯定;std::array的構造函數、析構函數和賦值操做符都是編譯器隱式聲明的……這讓不少用慣了std::vector這類容器的程序員不習慣,以爲std::array很差用。

但實際上,std::array的威力極可能被低估了。在這篇文章裏,我會從各個角度介紹下std::array的用法,但願能帶來一些啓發。c++

本文的代碼都在C++17環境下編譯運行。當前主流的g++版本已經能支持C++17標準,可是不少版本(如gcc 7.3)的C++17特性不是默認打開的,須要手工添加編譯選項-std=c++17程序員

自動推導數組大小

不少項目中都會有相似這樣的全局數組做爲配置參數:算法

uint32_t g_cfgPara[] = {1, 2, 5, 6, 7, 9, 3, 4};

當程序員想要使用std::array替換原生數組時,麻煩來了:編程

array<uint32_t, 8> g_cfgPara = {1, 2, 5, 6, 7, 9, 3, 4};  // 注意模板參數「8」

程序員不得不手工寫出數組的大小,由於它是std::array的模板參數之一。若是這個數組很長,或者常常增刪成員,對數組大小的維護工做恐怕不是那麼愉快的。有人要抱怨了:std::array的聲明用起來尚未原生數組方便,選它幹啥?

可是,這個抱怨只該限於C++17以前, C++17帶來了類模板參數推導特性, 你再也不須要手工指定類模板的參數:數組

array g_cfgPara = {1, 2, 5, 6, 7, 9, 3, 4};  // 數組大小與成員類型自動推導

看起來很美好,但很快就會有人發現不對頭:數組元素的類型是什麼?仍是std::uint32_t嗎?
有人開始嘗試只提供元素類型參數,讓編譯器自動推導長度,遺憾的是,它不會奏效。函數

array<uint32_t> g_cfgPara = {1, 2, 5, 6, 7, 9, 3, 4};  // 編譯錯誤

好吧,暫時看起來std::array是不能像原生數組那樣聲明。下面咱們來解決這個問題。工具

用函數返回std::array

問題的解決思路是用函數模板來替代類模板——由於C++容許函數模板的部分參數自動推導——咱們能夠聯想到std::make_pairstd::make_tuple這類輔助函數。巧的是, C++標準真的在TS v2試驗版本中推出過std::make_array 然而由於類模板參數推導的問世,這個工具函數後來被刪掉了。
但顯然,用戶的需求仍是存在的。因而在C++20中, 又新增了一個輔助函數std::to_array

別被C++20給嚇到了,這個函數的代碼其實很簡單,咱們能夠把它拿過來定義在本身的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>{});
}

細心的朋友會注意到,上面這個定義與C++20的推薦實現有所差別,這是有目的的。稍後我會解釋這麼幹的緣由。測試

如今讓咱們嘗試下用新方法解決老問題:ui

auto g_cfgPara = to_array<int>({1, 2, 5, 6, 7, 9, 3, 4});  // 類型不是uint32_t?

不對啊,爲何元素類型不是原來的std::uint32_t
這是由於模板參數推導對std::initializer_list的元素拒絕隱式轉換,若是你把to_array的模板參數從int改成uint32_t,會獲得以下編譯錯誤:

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,有點慘是不,繞了一圈回到原點,仍是不能強制指定類型。

這個時候,以前針對std::array作的修改派上用場了:我給to_array_impl增長了一個模板參數,讓輸入數組的元素和返回std::array的元素用不一樣的類型參數表示,這樣就給類型轉換帶來了可能。爲了實現轉換到指定的類型,咱們還須要添加兩個工具函數:

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>{});
}

這兩個函數和to_array的區別是:它帶有3個模板參數:第一個是要返回的std::array的元素類型,後兩個和to_array同樣。這樣咱們就能夠經過指定第一個參數來實現定製std::array元素類型了。

auto g_cfgPara = to_typed_array<uint32_t>({1, 2, 5, 6, 7, 9, 3, 4});  // 自動把元素轉換成uint32_t

這段代碼能夠編譯經過和運行,可是卻有類型轉換的編譯告警。固然,若是你膽子夠大,能夠在to_array_impl函數中放一個static_cast來消除告警。可是編譯告警提示了咱們一個不能忽視的問題:若是萬一輸入的數值溢出了怎麼辦?

auto g_a = to_typed_array<uint8_t>({256, -1});  // 數字超出uint8_t範圍

編譯器仍是同樣的會讓你編譯經過和運行,g_a中的兩個元素的值將分別爲0和255。若是你不明白爲何這兩個值和入參不同,你該複習下整型溢出與迴繞的知識了。

顯然,這個方案還不完美。但咱們能夠繼續改進。

編譯期字面量數值合法性校驗

首先能想到的作法是在to_array_impl函數中放入一個if判斷之類的語句,對於超出目標數值範圍的輸入拋出異常或者作其餘處理。這固然可行,但要注意的是這些工具函數是能夠在運行期調用的,對於這種經常使用的基礎函數來講,性能相當重要。一旦在裏面加入了錯誤判斷,意味着運行時的每一次調用性能都會降低。

理想的設計是:只有在編譯期生成的數組才進行校驗,而且報編譯錯誤。但運行時調用函數時不要加入任何校驗。

惋惜的是,至少在C++20以前,沒有辦法指定函數只容許在編譯期執行[2]。那有沒有其餘手段呢?

熟悉C++的人知道:C++的編譯期處理大多能夠用模板的trick來完成——由於模板參數必定是編譯期常量。所以咱們能夠用模板參數來完成編譯期處理——只要把數組元素所有做爲模板的非類型參數就能夠了。固然,這裏有個問題:模板的非類型參數的類型怎麼肯定?正好C++17提供了auto模板參數的功能,能夠派上用場:

template<typename T>
constexpr void CheckIntRanges() noexcept {}  // 用於終結遞歸

template<typename T, auto M, auto... N>
constexpr void CheckIntRanges() noexcept
{
    // 防止無符號與有符號比較
    static_assert(!((std::numeric_limits<T>::min() >= 0) && (M < 0)));

    // 範圍校驗
    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;
};

注意這個函數中,全部的校驗都經過static_assert完成。這就保證了校驗必定只會發生在編譯期,不會帶來任何運行時開銷。
DeclareArray的使用方法以下:

constexpr auto a1 = DeclareArray<uint8_t, 1, 2, 3, 4, 255>();  // 聲明一個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>();  // 編譯錯誤,-1超出uint8_t範圍
auto a3 = DeclareArray<uint16_t, 1, 2, 3, 65536>();  // 編譯錯誤,65536超出uint16_t範圍

這裏有一個誤區須要說明:有些人可能會把DeclareArray聲明成這樣:

template<typename T, T... N>  // 注意N的類型爲T
constexpr auto DeclareArray() noexcept

這麼作的話,會發現對數值的校驗老是能經過——由於模板參數在進入校驗以前就已經被轉換爲T類型了。若是你的編譯器不支持C++17的auto模板參數,那麼能夠經過使用std::uint64_tstd::int64_t這些「最大」的類型來間接達到目的。

另外一點要說明的是,C++對於非類型模板參數的容許類型存在限制,DeclareArray的方法只能用於數組元素爲基本類型的場景(至少在C++20之前如此)。可是這也足夠了。若是數組的元素是自定義類型,就能夠經過自定義的構造函數等方法來控制類型轉換。

若是你看到這裏以爲有點意思了,那就對了,後面還有更過癮的。

編譯期生成數組

C++11中新增的constexpr修飾符能夠在編譯期完成不少計算工做。可是通常constexpr函數只能返回單個值,一旦你想用它返回一串對象的集合,就會遇到麻煩:STL容器都有動態內存申請功能,不能做爲編譯期常量(至少在C++20以前如此);而原生數組做爲返回值會退化爲指針,致使返回懸空的指針。即便是返回數組的引用也是不行的,會產生懸空的引用。

constexpr int* Func() noexcept
{
    int a[] = {1, 2, 3, 4};
    return a;  // 嚴重錯誤!返回局部對象的地址
}

直到std::array的出現,這個問題才獲得較好解決。std::array既能夠做爲編譯期常量,又能夠做爲函數返回值。因而,它成爲了編譯期返回集合數據的首選。

在上面to_array等工具函數的實現中,咱們已經見過了編譯期返回數組是怎麼作的。這裏咱們再大膽一點,寫一個編譯期冒泡排序:

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;
}

由於整個排序算法都是在編譯期完成,因此咱們沒有必要太關注冒泡排序的效率問題。固然,只要你願意,徹底能夠寫出一個編譯期快速排序——畢竟constexpr函數也能夠在運行期使用,很差說會不會有哪一個憨憨在運行時調用它。

在編寫constexpr函數時,有兩點須要注意:

1. constexpr函數中不能調用非constexpr函數。所以在交換元素時不能用std::swap,排序也不能直接調用std::sort

2. 傳入的數組是constexpr的,所以參數類型必須加上const,也不能對數據進行就地排序,必須返回一個新的數組。

雖然限制不少,但編譯期算法的好處也是巨大的:若是運算中有數組越界等未定義行爲,編譯將會失敗。相比起運行時的測試,編譯期測試constexpr函數能有效的提早攔截問題。並且只要編譯經過就意味着測試經過,比起額外跑白盒測試用例方便多了。

上面的一大串static_assert語句讓人看了不舒服。這麼寫的緣由是std::arrayoperator==函數並不是constexpr(至少在C++20前如此)。可是咱們也能夠本身定義一個模板函數用於判斷兩個數組是否相等:

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和原生數組
    static_assert(!Equals(before, after));  // 比較兩個std::array
    return 0;
}

咱們定義的Equalsstd::array的比較運算符更強大,甚至能夠在std::array和原生數組之間進行比較。

對於Equals有兩點須要說明:

1. std::size是C++17提供的工具函數,對各類容器和數組都能返回其大小。固然,這裏的Equals只會容許編譯期肯定大小的容器傳入,不然觸發編譯失敗。

2. Equals定義了兩個版本,這是被C++的一個限制所逼的無可奈何:C++禁止{...}這種std::initializer_list字面量被推導爲模板參數類型,所以咱們必須提供一個版本聲明參數類型爲數組,以便{1, 2, 3, 4}這種表達式能做爲參數傳進去。

編譯期排序是一個啓發性的嘗試,咱們能夠用相似的方法生成其餘的編譯期集合常量,好比指定長度的天然數序列:

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;
}

這段代碼的編譯運行都沒有問題,但它並非推薦的作法。緣由是在NaturalNumbers函數中,先定義了一個內容全0的局部數組,而後再挨個修改它的值,這樣沒有直接返回指定值的數組效率高。有人會想能不能把arr的初始化給去掉,但這樣會致使編譯錯誤——constexpr函數中不容許定義沒有初始化的局部變量。

可能有人以爲這些計算都是編譯期完成的,對運行效率沒影響——可是不要忘了constexpr函數也能夠在運行時調用。更好的作法能夠參見前面to_array函數的實現,讓數組的初始化一鼓作氣,省掉挨個賦值的步驟。

咱們用這個新思路,寫一個通用的數組生成器,它能夠接受一個函數對象做爲參數,經過調用這個函數對象來生成數組每一個元素的值。下面的代碼還演示了下如何用這個生成器在編譯期生成奇數序列和斐波那契數列。

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來定製要生成的數字序列(限定C++17)
    constexpr auto specified = GenerateArray<3>([](size_t i) { return i + 10; });
    static_assert(Equals(specified, {10, 11, 12}));
    return 0;
}

最後那個傳入lambda來定製數組的作法存在一個疑問:lambdaconstexpr函數嗎?答案爲:能夠是,但須要C++17支持。

GenerateArray這個數組生成器將會在後面發揮重大做用,繼續往下看。

截取子數組

std::array並未提供輸入一個指定區間來創建新容器的構造函數,可是藉助上面的數組生成器,咱們能夠寫個輔助函數來實現子數組生成操做(這裏再次用上了lambda函數做爲生成算法)。

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字面量爲原始數據
    constexpr auto x = SubArray<3>({1, 2, 3, 4, 5, 6}, 2);  // 下標爲2開始,取3個元素
    static_assert(Equals(x, {3, 4, 5}));

    // 以std::array爲原始數據
    constexpr auto x1 = SubArray<2>(x, 1);  // 下標爲1開始,取2個元素
    static_assert(Equals(x1, {4, 5}));

    // 以原生數組爲原始數據
    constexpr uint8_t a[] = {9, 8, 7, 6, 5};
    constexpr auto y = SubArray<2>(a, 3);
    static_assert(Equals(y, {6, 5}));  // 下標爲3開始,取2個元素

    // 以字符串爲原始數據,注意生成的數組不會自動加上'\0'
    constexpr const char* str = "Hello world!";
    constexpr auto z = SubArray<5>(str, 6);
    static_assert(Equals(z, {'w', 'o', 'r', 'l', 'd'}));  // 下標爲6開始,取5個元素
 
    // 以std::vector爲原始數據,非編譯期計算
    vector<int32_t> v{10, 11, 12, 13, 14};
    size_t n = 2;
    auto d = SubArray<3>(v, n);  // 運行時生成數組
    assert(Equals(d, {12, 13, 14}));  // 注意不能用static_assert,不是編譯期常量
    return 0;
}

使用SubArray時,模板參數N是要截取的子數組大小,入參t是任意能支持下標操做的類型,入參base是截取元素的起始位置。因爲std::array的大小在編譯期是肯定的,所以N必須是編譯期常量,但參數base能夠是運行時變量。

當全部入參都是編譯期常量時,生成的子數組也是編譯期常量。

SubArray提供了兩個版本,目的也是爲了讓std::initializer_list字面量能夠做爲參數傳入。

拼接多個數組

採用相似的方式能夠作多個數組的拼接,這裏一樣用了lambda做爲生成函數。

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};  // 原生數組
    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個數組拼接在一塊兒
    static_assert(Equals(x, {1, 2, 3, 4, 5, 6, 7, 8}));
    return 0;
}

和以前同樣,ConcatArrays使用了模板參數來同時兼容原生數組和std::array,它甚至能夠接受任何編譯期肯定長度的自定義類型參與拼接。

ConcatArrays函數由於可變參數的語法限制,沒有再對std::initializer_list字面量進行適配,這致使std::initializer_list字面量不能再直接做爲參數:

constexpr auto x = ConcatArrays(a, {4, 5, 6});  // 編譯錯誤

可是咱們有辦法規避這個問題:利用前面介紹過的工具把std::initializer_list先轉成std::array就能夠了:

constexpr auto x = ConcatArrays(a, to_array({4, 5, 6}));  // OK

編譯期拼接字符串

std::array適合用來表示字符串麼?回答這個問題前,咱們先看看原生數組是否適合表示字符串:

char str[] = "abc";  // str數組大小爲4,包括結尾的'\0'

上面是很常見的寫法。因爲數組名可退化爲指針,str可用於各類須要字符串的場合,如傳給cout打印輸出。

std::array做爲對原生數組的替代,天然也適合用來表示字符串。有人可能會以爲std::array無法直接做爲字符串類型使用,不太方便。但實際上只要調用data方法,std::array就會返回能做爲字符串使用的指針:

constexpr auto str = to_array("abc");  // to_array能夠將字符串轉換爲std::array
static_assert(str.size() == 4);
static_assert(Equals(str, "abc"));  // Equals也能夠接受字符串字面量
cout << str.data();  // 打印字符串內容

因爲字符串字面量是char[]類型,所以前面所編寫的工具函數,均可以將字符串做爲輸入參數。上面的Equals只是其中一個例子。

那以前寫的數組拼接函數ConcatArrays能用於拼接字符串麼?能,但結果和咱們想的有差別:

constexpr auto str = ConcatArrays("abc", "def");
static_assert(str.size() == 8);  // 長度不是7?
static_assert(Equals(str, {'a', 'b', 'c', '\0', 'd', 'e', 'f', '\0'}));

由於每一個字符串結尾都有'\0'結束符,用數組拼接方法把它們拼到一塊兒時,中間的'\0'沒有被去掉,致使結果字符串被切割爲了多個C字符串。

這個問題解決起來也很容易,只要在拼接數組時把全部數組的最後一個元素('\0')去掉,而且在返回數組的末尾加上'\0'就能夠了。下面的代碼實現了字符串拼接功能,非類型參數E是字符串的結束符,一般爲'\0',可是也容許定製。咱們甚至能夠利用它來拼接結束符爲其餘值的對象,好比消息、報文等。

// 最後一個字符,放入結束符
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) {  // 結束符不容許出如今字符串中間
            throw "terminator in the middle";
        }
        return p[i];
    }
    if (p[size(p) - 1] != E) {  // 結束符必須是最後一個字符
        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 ";  // 原生數組形式的字符串
    constexpr auto b = to_array("love ");  // std::array形式的字符串
    constexpr auto str = ConcatStrings(a, b, "C++");  // 拼接 數組 + std::array + 字符串字面量
    static_assert(Equals(str, "I love C++"));
    return 0;
}

這段代碼中用了兩個throw,這是爲了校驗輸入的參數是否都爲合法的字符串,即:字符串長度=容器長度-1。若是不符合該條件,會致使拼接結果的長度計算錯誤。

當編譯期的計算拋出異常時,只會出現編譯錯誤,所以只要不在運行時調用ConcatStrings,這兩個throw語句不會有更多影響。但由於這個校驗的存在,強烈不建議在運行期調用ConcatStrings作拼接,況且運行期也不必用這種方法——std::string的加法操做它不香麼?

有人會想:可否在編譯期計算字符串的實際長度,而不是用容器的長度呢?這個方法看似可行,定義一個編譯期計算字符串長度的函數確實很容易:

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把一個字符串按實際長度轉成std::array
    constexpr auto str = SubArray<StrLen(g_str) + 1>(g_str, 0);
    static_assert(Equals(str, "abc"));
    return 0;
}

可是,一旦你試圖把StrLen放到ConcatStrings的內部去聲明數組長度,就會產生問題:C++的constexpr機制要求只有在能看到輸入參數的constexpr屬性的地方,才容許StrLen的返回結果肯定爲constexpr。而在函數內部時,看到的參數類型並非constexpr

固然咱們能夠變通一下,作出一些有趣的工具,好比使用萬惡的宏:

// 把一個字符串按實際長度轉成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]。若是你狠得下心使用變參宏,甚至能夠定義出按實際字符串長度計算結果數組長度的更通用拼接函數。但我嚴重懷疑這種需求的必要性——畢竟咱們只是作編譯期的拼接,而編譯期的字符串不該該會有結束符位置不在末尾的場景。

看到這裏的人,或多或少該佩服一下std::array的強大了。上面這些編譯期操做,用原生數組很難完成吧?

展望C++20——打破更多的枷鎖

我在文章中說了多少次「至少在C++20以前如此」?不記得了,可是能肯定的是:C++20會帶來不少美好的東西:std::array會有constexpr版本的比較運算符; 函數能夠用consteval限定只在編譯期調用; 模板非類型參數容許更多的類型;STL容器對象能夠做爲constexpr常量……全部這一切,都只是C++20的minor更新而已,在絕大多數的特性介紹中,它們連提都不會被提到!

可想而知,用上C++20之後,編程會發生多大的變化。那時咱們再來找找更多有趣的用法

尾註

[1] to_array定義了兩個版本,分別以左值引用和右值引用做爲參數類型。按照C++11的最優實踐,這樣的函數本應該只定義一個版本而且使用完美轉發。可是to_array的場景若是用萬能引用會帶來一個問題:C++禁止std::initializer_list字面量{...}被推導爲模板類型參數,完美轉發方案會致使std::initializer_list字面量不能做爲to_array的入參。在後面內容中咱們會看到屢次這個限制所帶來的影響。

[2] C++20加入了consteval修飾符, 能夠指定函數只容許在編譯期調用。

[3] 須要注意的是:constexpr用於修飾指針時,表示的是指針自己爲常量(而不是其指向的對象)。和const不一樣,constexpr並不容許放在類型聲明表達式的中間。所以若是要在編譯期計算一個constexpr指針指向的字符串長度,這個字符串必須位於靜態數據區裏,不能位於棧或者堆上(不然其地址沒法在編譯期肯定)。

本文分享自華爲雲社區《C++語言中std::array的神奇用法總結》,原文做者:飛得樂。

 

點擊關注,第一時間瞭解華爲雲新鮮技術~

相關文章
相關標籤/搜索