Saturday, August 12, 2023

淺談 C++ 的 requires 關鍵字

引言
使用 Template 模板的時候,如果可以加上 requires 關鍵字,就可以對不同的資料類型,實體化出不同的的函式,或者做編譯時期的檢查。

例子

#include <iostream>

template<typename T>
T addition(T a, T b)
{
constexpr bool valid = requires (T x) { x + x; };
if (valid)
{
return a + b;
}
else
{
return T{};
}
}

int main(int argc, char** argv)
{
std::cout << addition(1, 2) << std::endl;
return 0;
}

執行結果
3

說明
當我們在函式中使用了 constexpr 和 requires,它就會在編譯時期把檢查做完,然後只實體化相關部分的模板。用以上的 addition 為例,使用這個方法,我們就能使用一個模板,對所有不同的類型進行支持,而不需要不斷以不同的輸入類型,去重載同一個函式。

其實 requires 的用法還有很多,不過為了簡單,就先說到這裏吧。

Tuesday, July 4, 2023

淺談 C++ 20 的 std::span 跨類

引言
自 C++ 20 開始,針對所有順序排列的矩陣,標準庫開始提供了一種新的寫法。

以往,我們在處理不同容器 (Container) 的時候,即使它們十分相似,我們也要分開續一處理。比如說,std::array,std::vector,還有最傳統的 C-style array,即使它們在記憶體中的排列,基本上是完全一樣的,但只因名稱不同,我們就需要寫三個不同的函式去處理它。而針對傳統的 C-style array,我們更需要自行使用 sizeof(array)/sizeof(type) 去找出陣列的大小,情況的確十分麻煩。

而自從 C++ 20 之後,情況簡單太多了:一個 std::span 即可解決一切。

---

範例及解說
你可以把 std::span 想像成是所有順序排列矩陣的模板,然後再加上 string_view 的邏輯。

這裏的模板是指,我們只需要使用 std::span 實作一次,不同的矩陣都能自動支援該函式。而 string_view 是指,std::span 只一個視圖:它的作用,只是指向某一個矩陣,它本身並不擁有該矩陣的記憶體。雖然我們能透過 std::span 去讀寫矩陣中每一個元素 (element), 但我們卻不能改變該矩陣本身的生命周期 (lifecycle)。換句話說,它只是指向該物件,但不能保證該物件仍然在 stack / heap 中。

以下這一個簡單的範例,就可解釋模板 (Template Class) 的情況:

#include <iostream>
#include <vector>
#include <span>

template<typename T>
void printSequence(std::span<T> sequence)
{
    for (T& s: sequence)
    {
        std::cout << s << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::vector<int> v1   = {1, 2, 3, 4};
    std::array<int, 4> a1 = {2, 3, 4, 5};
    int ca1 []            = {3, 4, 5, 6};

    std::cout << "printSequence vector" << std::endl;
    printSequence<int>(v1);

    std::cout << "printSequence array" << std::endl;
    printSequence<int>(a1);

    std::cout << "printSequence c-style array" << std::endl;
    printSequence<int>(ca1);

    return 0;
}

程式的執行結果,則是如下:

printSequence vector
1 2 3 4
printSequence array
2 3 4 5
printSequence c-style array
3 4 5 6 

由此可見,std::span 就像一個支援 std::vector / std::array / c-style array 的模板一樣,我們只需要使用 std::span 實作函式一次,即可自動獲得所有不同矩陣類型的函式支援。

以下的另一個例子,則說明 std::span 與 string_view 相似的地方。
std::span 只是一個視圖 (View),所以它實際上只是指針,並不能控制矩陣的生命周期:

#include <iostream>
#include <vector>
#include <span>

template<typename T>
void printSequence(std::span<T> sequence)
{
    for (T& s: sequence)
    {
        std::cout << s << " ";
    }
    std::cout << std::endl;
}

int main() {

    std::span<int> my_span;

    {
        std::vector<int> my_vector = {1, 2, 3, 4};
        my_span = std::span<int>{my_vector};

        std::cout << "inside scope" << std::endl;
        printSequence<int>(my_span);
    }

    std::cout << "outside scope" << std::endl;
    printSequence<int>(my_span);

    return 0;
}

程式執行結果如下:

inside scope
1 2 3 4
outside scope
-353796048 25208 2043 0 

由於 my_vector 在 "outside scope" 那行已經被消滅掉,所以打印出來的 my_span 只會是一堆亂碼。由此可見,my_span 並不能改變 my_vector 的作用域 (scope),它充其量只是指針。

---

小結
使用 std::span 可以簡單地解決不同矩陣的列舉和更新。尤其是傳統的 c-style array,使用 std::span 即可使 c-style array 也能自動更援不同 iterator 和 for-each 等新功能。不過,由於 std::span 不能控制物件的生命周期,所以使用的時候也要小心,避免存取一些早已消失了的物件。

---

參考:https://learn.microsoft.com/en-us/cpp/standard-library/span-class?view=msvc-170

Saturday, May 20, 2023

淺談 C++ 的 if constexpr 分支忽略

簡介
以往定義常數,我們都會採用 const 這個關鍵字。雖然它可以確保該常數不變 (immutable),但要注意一點:它並不保證數值是在編譯期間決定的。例如,在賦值的時候,這種寫法是被允許的:

int x = getFromSomewhere();
const int MAX_COUNT = 30 + x;

這樣的話,雖然 MAX_COUNT 是不變的,但我們就不能在編譯期間決定 MAX_COUNT 的數值。這導致程式變得難以除錯,還有就是執行效率難以保證。為了解決這種情況,自 C++11 開始,官方決定引入 constexpr 關鍵字,確保數值必須在編譯期間決定:

constexpr int MAX_COUNT = 30;

這種寫法,雖然會增加編譯的時間,但就避免了程式執行期間的額外開銷,以及降低了日後除錯的難度。而從 C++ 17 開始,為了降低編譯的時間,還有減少程式的體積,constexpr 更開始支援一種新的玩法:只要加上 if 關鍵字,它就能忽略掉指定的分支 (Branch Discard)。

(其實玩法還有很多,但這次就只提及一個最簡單的情況)

---

跟模板 (Template) 一起使用的 if constexpr
有些時候,我們會想用模板寫一個萬能的函式, 然後根據它的資料類型,會有不同的處理方法。以下的例子,是一個可以接受任何類型(T)的函式。當它收到 double 類型,就會執行第一個分支;收到 int 類型,就會執行第二個分支;收到其他類型,就會執行 else 分支:

#include <iostream>

template<typename T>
void testFunction(T objT)
{
    if constexpr (std::is_same_v<T, double>) {

        // Interestingly, compiler ignores template-related syntax:
        objT.function_never_existed();
        T::this_function_does_not_exist_but_it_compiles();

        // However. doing this will still make compilation fail:
        // functionNeverExisted();
    }
    else if constexpr (std::is_same_v<T, int>)
    {
        std::cout << "Integer detected: " << objT << std::endl;
    }
    else
    {
        std::cout << "Else branch for testFunction!" << std::endl;
    }
}

int main(int argc, char* argv[])
{
    testFunction(static_cast<int>(123));
    return 0
}


使用 C++17 或以上,是可以成功編譯的。而程式執行的結果是:

Integer detected: 123

---

解說
這種寫法,如果只使用 C++11 的話,它是根本不能編譯的:

clang++ main.cpp --std=c++11

它會告知 if_same_v 並不存在,還有就是第一分支中,存在不正確的表達式。

但如果使用 C++17 / C++20 的話,就能編譯成功:

clang++ main.cpp --std=c++17
clang++ main.cpp --std=c++2a

事實上,在第一分支中,其實有很多敍述,包括 function_never_existed 是根本不存在的。但它仍然能成功編譯,是因為編譯器一早已經明白,這個 if constexpr 分支內的模板,根本不會被實體化,所以它就會自動把這個部分,與模板相關的表達式全部忽略。但有趣的是,其他非模板的部分,雖然它們最後都會被省略,但在編譯的過程中,仍然會被一一檢查。

所以,如果我們將範例中的 functionNeverExisted() 中的注釋解除,它將會在編譯期間,產生以下的編譯錯誤:

main.cpp:15:3: error: use of undeclared identifier 'functionNeverExisted'
                functionNeverExisted();
                ^
1 error generated.

長話短說就是:C++17 雖然會將分支省略,卻仍不允許在非模板的部分中,有任何違規的寫法。

---

小結
自從 C++17 後,我們可以利用 if constexpr,在編譯期間進行分支忽略,令程式寫法更簡單、執行效率更高,以及可以只寫一個模板函式,就能支援(幾乎所有)不同的資料類型。