template 型別的需求描述:C++20 concepts

| | 0 Comments| 15:10
Categories:

Template 是 C++ 一個泛型的重要功能。透過 template 可以讓開發者只要寫一次,就可以針對不同的型別的資料,來做處理。

他雖然用起來很方便,但是大部分情況下都缺少對於需求型別的描述,所以如果沒用好寫錯了,就很有可能因為使用了不符合需求的型別,而導致無法正確編譯;而這個時候,編譯器給個錯誤訊息往往也會過於雜亂、讓開發者難以理解、也難以找到真正的問題點。

比如說下面這個簡單的範例程式:

struct A
{
  int value;
};
template<typename T>
auto get_value(const T& a)
{
  auto v = a.value;
  // do some thing
  return v;
}
int main()
{
  A a{ 10 };
  auto x = get_value(a);
}

這個程式裡面,get_value() 這個函式會去取得傳入參數 avalue 這個成員變數的值,然後經過處理後、回傳結果的值。

上面的程式由於 A 這個結構有定義 value,所以使用上並沒有問題,可以正確編譯、也可以正確執行。

但是如果其他想要使用 get_value() 這個函式的人不知道使用的型別需要有 value 這個成員的話,那會怎樣呢?比如說,把傳入參數的型別從 A 變成 int 的話,main() 會變成:

int main()
{
  int a{ 10 };
  auto x = get_value(a);
}

這時候,就會出現錯誤而無法完成編譯了~

而在 Visual Studio 中的編譯錯誤,訊息會如下:

1>1>concept.cpp
1>concept.cpp(9,1): error C2228: '.value' 的左邊必須有類別/結構/等位
1>concept.cpp(9,1): message : 類型為 'const T'
1>concept.cpp(9,1): message :         with
1>concept.cpp(9,1): message :         [
1>concept.cpp(9,1): message :             T=int
1>concept.cpp(9,1): message :         ]
1>concept.cpp(17): message : 請參考正在進行編譯的函式 樣板 具現化 'auto get_value(const T &)'
1>concept.cpp(17): message :         with
1>concept.cpp(17): message :         [
1>concept.cpp(17): message :             T=int
1>concept.cpp(17): message :         ]
1>concept.cpp(11,1): error C3536: 'v': 無法在初始化之前使用
1>專案 "Concept.vcxproj" 建置完成 -- 失敗。

而彙整的錯誤報告有兩個,分別是:

  • 錯誤 C2228:'.value' 的左邊必須有類別/結構/等位 S:CppTestConceptconcept.cpp 9
  • 錯誤 C3536:'v': 無法在初始化之前使用 S:CppTestConceptconcept.cpp 11

如果點選上面的錯誤、Visual Studio 所跳到的程式碼,都會在 get_value() 內,讓人較難以判斷;而如果程式的 template 用得更複雜的話,在不熟悉這類的錯誤、同時也難以認真去讀編譯器輸出的訊息(message)的狀況下,那要判斷錯誤原因的狀況,是相當困難的…


為了解決這樣的問題,C++20 引進了所謂的「concepts」,用來針對 template 的型別,做額外的描述。

這部分的介紹可以參考《Constraints and concepts》或《C++20: Concepts, the Details》。而 Visual Studio 2019 在 16.3 後,也已經有部分支援 concepts(參考《C++20 Concepts Are Here in Visual Studio 2019 version 16.3》)了~這篇的程式測試,基本上都是在 Visual Studio 上測試的。

不過老實說由於這東西應該還算是在很早期的階段,所以相關介紹感覺還不算完整,Heresy 自己邊看邊摸,也不知道到底是不是完全正確;如果有錯的話,也麻煩指正一下了。

總之,如果要使用 concept 的話,上面的程式碼大致上就會修改成類似下面的樣子:

template<typename T>
concept have_value = requires(T a) {
  a.value;
};
template<typename T>
requires have_value<T>
auto get_value(const T& a)
{
  auto v = a.value;
  // do some thing
  return v;
}
int main()
{
  int b = 10;
  auto y = get_value(b);
}

其中,第一段就是在定義一個名為「have_value」的 concept。

template<typename T>
concept have_value = requires(T a) {
  a.value;
};

在 C++20 中,所謂的 concept 基本上就是一些「需求」(requirement)的集合,用來驗證型別是否符合需求用的。

而這個例子裡面,就是簡單地透過驗證 a.value; 這個式子是否合法、來確定型別 T 是否有 value 這個成員。

在定義好了 have_value 這個 concept 後,接下來則是幫本來的 get_value() 加上一行

requires have_value<T>

告訴編譯器以及使用者,這個函式的 template 型別 T 需要符合 have_value 這個 concept。

在這樣修改完成後,編譯的錯誤會變成是:

1>concept.cpp
1>concept.cpp(21,11): error C2672: 'get_value': 找不到相符的多載函式
1>concept.cpp(21,22): error C7602: 'get_value': 未滿足相關聯的限制式
1>concept.cpp(13): message : 請參閱 'get_value' 的宣告
1>專案 "Concept.vcxproj" 建置完成 -- 失敗。

基本上,錯誤算是更明確了!

而如果點選上面的錯誤的話,他也會跳到呼叫 main() 裡面呼叫 get_value() 的地方,而不會再直接進到 get_value() 內~

除了錯誤訊息更明確外,由於在 get_value() 宣告就明確地寫了它需要符合的條件,對於要使用它的開發者,也會更容易地去判斷自己要使用的型別,是否符合函式的設計了~


在 template 函式要使用,其實總共有三種寫法。第一種寫法(Requires Clause),就是像上面一樣,寫在 template<> 的後面的形式:

template<typename T>
requires have_value<T>
auto get_value(const T& a){ ... }

第二種方法(Trailing Requires Clause),則是把 requires 加在後面:

template<typename T>
auto get_value(const T& a) requires have_value<T>{ ... }

另外,甚至可以直接寫成下面更為精簡的形式(Constrained Template Parameters):

template<have_value T>
auto get_value(const T& a) { ... }

而除了針對 template 函式來做需求的限制外,他也可以用來做 template specialization。

比如說上面的程式就可以寫成:

struct A
{
  int value;
};
template<typename T>
concept have_value = requires(T a) {
  a.value;
};
template<have_value T> // have_vale
auto get_value(const T& a)
{
  auto v = a.value;
  // do some thing
  return v;
}
template<typename T> // generic
auto get_value(const T& a)
{
  auto v = a;
  // do some thing
  return v;
}
int main()
{
  A a{ 10 };
  auto x = get_value(a); // use have_vale
  int b = 10;
  auto y = get_value(b); // use generic
}

透過這樣撰寫兩個不同的 get_value(),就可以讓編譯器去判斷呼叫的時候要去用哪的版本了。在這個狀況下,只要所傳入的參數型別符合 have_value 這個 concept 的話,他就會去使用第一個版本,否則就會去使用第二個一般性的版本。

所以透過 concept 來做 template specialization,其實也算是滿實用的~


而除了用在 template function 上,cooncept 也能用在 template class 上。例如:

template<typename T>
requires have_value<T>
class CValue {};

或是

template<have_value T>
class CValue {};

此外,他也可以用在 template class 的 member funtion;例如:

template<typename T>
class CValue
{
public:
  T  m_val;
  int get_value() const
  {
    return m_val;
  }
  int get_value() const requires have_value<T>
  {
    return m_val.value;
  }
};

以上面的例子來說,雖然在 CValue 這個類別裡面同時有宣告兩個 get_value() 的成員函式,但是由於條件(requires)不同,所以其實並不會同時存在,所以並不會出現重複定義的狀況。

而在使用時,編譯器也會根據型別是否符合 have_value 這個 concept,來產生不同的版本。


這篇大概就先記錄這些,下一篇再來整理一下怎麼定義一個 concept 吧~


主要參考:

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *