C++霧中風景16:std::make_index_sequence, 來試一試新的黑魔法吧

HappenLee發表於2021-01-01

C++14在標準庫裡新增了一個很有意思的元函式: std::integer_sequence。並且通過它衍生出了一系列的幫助模板:
std::make_integer_sequence, std::make_index_sequence, std:: index_sequence_for。在新的黑魔法的加持下,它可以幫助我們完成在編譯期間獲取了一組編譯期整數的工作。
接下來請繫好安全帶,準備發車,和大家聊聊新的黑魔法:std::make_index_sequence

1.what's std::make_index_sequence

1.1 舉個例子

筆者這裡先從一個簡單的例子展開,先帶大家看看std::make_index_sequence是如何使用的。

在C++之中有一個很常見的需求,定義一組編譯期間的陣列作為常量,並在執行時或者編譯時利用這些常量進行計算。現在假如我們需編譯期的一組1到4的平方值。你會怎麼辦呢?

嗯.... 思考一下,可以這些寫:

constexpr static size_t const_nums[] = {0, 1, 4, 9, 16};

int main() {
    static_assert(const_nums[3] == 9); 
}

Bingo, 這個程式碼肯定是正確的,但是如果4擴充套件到了20或者100?怎麼辦呢?

嗯~~,先彆著急罵髒話,我們可以用std::make_index_sequencestd::index_sequence來幫助我們實現這個邏輯:

template <size_t ...N>
static constexpr auto square_nums(size_t index, std::index_sequence<N...>) {
    constexpr auto nums = std::array{N * N ...};
    return nums[index];
}

template <size_t N>
constexpr static auto const_nums(size_t index) {
    return square_nums(index, std::make_index_sequence<N>{});
}

int main() {
    static_assert(const_nums<101>(100) == 100 * 100); 
}

這程式碼咋看之下有些嚇人,不過沒關係,我們來一點點的庖丁解牛的解釋清楚它是如何工作的。

1.2 做個解釋

我們來拆解一下1.1的程式碼,首先我們定義了一個constexpr的靜態函式const_nums。它通過我們本文的主角std::make_index_sequence來構造了一組0,1,2,3 .... N - 1的一組編譯器的可變長度的整數列。(注意,這裡呼叫std::make_index_sequence{}的建構函式沒有任何意義,純粹只是利用了它能夠生成編譯期整數列的能力。)
const_nums函式

接著我們來看squere_num函式,這就是我們實際進行平方計算,並生成編譯期靜態陣列的地方了,它的實現很簡單,就是依次展開通過std::make_index_sequence生成的數字,並進行平方計算,最後塞到std::array的建構函式之中進行構造。

2. How std::make_index_sequence

通過一個簡單的栗子,大家想必已經見識到這個新的黑魔法的獨特之處了。接下來,我們進一步的來剖析它的實現吧。

2.1 std::integer_sequence
template< class T, T... Ints >
class integer_sequence;

要了解std::make_index_sequence是如何工作的,就得先看看它的基礎類:std::integer_sequence。由上面的程式碼看,它很簡單,就是一個int型別,加上一組int數字,其實原理就是生成一組T型別的編譯期間數字序列。它本質上就是個空類,我們就是要獲取這個編譯期的數字序列。

2.2 std::index_sequence
template<size_t.. Ints>
using index_sequence = std::integer_sequence<size_t,Ints...>;

通常我們不會直接使用std::integer_sequence,而是通過定義一個size_tstd::integer_sequnece命名為index_sequence

2.3 std::make_index_sequence

這裡就是生成了一組數字序列0,1,2,3...N - 1的一組std::index_sequence。然後就可以利用這組序列的數字做任何我們想做的事情了。

那麼問題來了,std::make_index_sequence是如何實現的呢?

  • 可以通過超程式設計,生成N個元函式類,依次生成0到N - 1的序列,感興趣的話可以參考這個連結
  • 實際是由編譯器內部在編譯期間實現的,並不是基於現有的超程式設計的邏輯。

3.std::make_index_sequence與std::tuple

通過第二節的介紹,想必大家應該瞭解了std::make_index_sequence的實現原理了。接下來將介紹它最為重要的使用場景:與tuple的結合。

現在請大家思考一個問題:如何遍歷一個std::tuple。(不能使用C++17的std::apply

這個時候就要再次請出我們今天的主角,使用std::make_index_sequnce和lambda表示式來完成這個工作了。我們來看下面這部分程式碼:

template <typename Tuple, typename Func, size_t ... N>
void func_call_tuple(const Tuple& t, Func&& func, std::index_sequence<N...>) {
    static_cast<void>(std::initializer_list<int>{(func(std::get<N>(t)), 0)...});
}

template <typename ... Args, typename Func>
void travel_tuple(const std::tuple<Args...>& t, Func&& func) {
    func_call_tuple(t, std::forward<Func>(func), std::make_index_sequence<sizeof...(Args)>{});
}

int main() {
    auto t = std::make_tuple(1, 4.56, "happen lee");
    travel_tuple(t, [](auto&& item) {
        std::cout << item << ",";
    });
}
  • 這個程式碼首先定義了一個travel_tuple的函式,並且利用了std::make_index_sequence將tuple型別的引數個數進行了展開,生成了0到N - 1的編譯期數字。

  • 接下來我們再利用func_call_tuple函式和展開的編譯期數字,依次呼叫std::get<N>(tuple),並且通過lambda表示式依次的呼叫,完成了遍歷tuple的邏輯。

嗯,標準庫表示它也是這樣想的,所以C++17利用了std::make_index_sequence實現了std::apply,開啟了滿螢幕堆滿tuple的C++新時代了~~

4.小結

C++14新提供的std::make_index_sequence給了我們在編譯期操作tuple提供了更加便利的工具,並且在編譯期間的整數列也能夠幫助我們實現更多新的黑魔法。

大家可以嘗試自己用超程式設計實現了一個std::make_index_sequence, 筆者覺得這是一個很有意思的挑戰。

關於std::make_index_sequence就聊到這裡。希望大家能夠有所收穫,筆者水平有限。成文之處難免有理解謬誤之處,歡迎大家多多討論,指教。

5.參考資料

cppreference
make_index_sequence的原理

相關文章