天天看點

《STL源碼分析》學習筆記 — C++20 concepts一、什麼是concepts二、concepts 的定義三、constraints四、requires 子句五、requires 表達式六、requires 表達式中的 requirements 分類七、限制的偏序

《STL源碼分析》學習筆記 — C++20 concepts

  • 一、什麼是concepts
  • 二、concepts 的定義
  • 三、constraints
    • 1、Conjunctions
    • 2、Disjunctions
    • 3、Atomic constraints
    • 4、限制标準化
    • 5、限制的确定
  • 四、requires 子句
  • 五、requires 表達式
  • 六、requires 表達式中的 requirements 分類
    • 1、簡單要求
    • 2、類型要求
    • 3、複合要求
    • 4、嵌套要求
  • 七、限制的偏序

在C++20之前的版本,疊代器相關類型的擷取都是通過我們前面學習過的 iterator_traits。這個結構比較好了解。在C++20及之後,C++中提供了兩個新的關鍵字 concept 和 requires 用于定義概念。

一、什麼是concepts

在C++參考手冊中,是這樣描述概念的:

Class templates, function templates, and non-template functions (typically members of class templates) may be associated with a constraint, which specifies the requirements on template arguments, which can be used to select the most appropriate function overloads and template specializations.

Named sets of such requirements are called concepts. Each concept is a predicate, evaluated at compile time, and becomes a part of the interface of a template where it is used as a constraint

類模闆、函數模闆、非模闆方法(特指類模闆中的方法)可能會與某個限制相關聯,用以特化對模闆參數的要求。這可以被用來選擇最合适的函數重載和模闆特化。

這樣的具名要求集合被稱為概念。每個概念是一個斷言,在編譯時評估,而且該概念作為限制使用的模闆的一部分。是以 concept 的違反将在編譯時被檢測到:

std::list<int> l = {3,-1,10};
std::sort(l.begin(), l.end()); 
//Typical compiler diagnostic without concepts:
//  invalid operands to binary expression ('std::_List_iterator<int>' and
//  'std::_List_iterator<int>')
//                           std::__lg(__last - __first) * 2);
//                                     ~~~~~~ ^ ~~~~~~~
// ... 50 lines of output ...
//
//Typical compiler diagnostic with concepts:
//  error: cannot call std::sort with std::_List_iterator<int>
//  note:  concept RandomAccessIterator<std::_List_iterator<int>> was not satisfied
           

上述代碼中 sort 算法需要的疊代器為随機通路疊代器,是以編譯無法通過。

二、concepts 的定義

使用 concept 關鍵字的文法為:

template < template-parameter-list >
concept concept-name = constraint-expression;
           

如:

template <class T, class U>
concept Derived = std::is_base_of<U, T>::value;
           

concept 不能遞歸依賴自身,其模闆參數也不能使用限制。除此之外,concept 的顯式執行個體化、顯式特化、部分特化都是不支援的。

concept 可以被用于 id 表達式中。當其限制被滿足時,表達式為真;否則, 表達式為假:

#include <iostream>

template<typename T>
concept dereferenceable = requires (T a) 
{
	*a;
};

int main()
{
	constexpr bool bl1 = dereferenceable<int>;
	std::cout << bl1 << std::endl;
	
	constexpr bool bl2 = dereferenceable<int*>;
	std::cout << bl2 << std::endl;
}
           
《STL源碼分析》學習筆記 — C++20 concepts一、什麼是concepts二、concepts 的定義三、constraints四、requires 子句五、requires 表達式六、requires 表達式中的 requirements 分類七、限制的偏序

concept 還能被用于類型限制中,作為類型模闆參數聲明、占位類型說明符 以及複合要求中。其中,concept 被用于占位類型說明符主要是用于限制推導的類型:

#include <iostream>

template<typename T>
concept dereferenceable = requires (T a) 
{
	*a;
};

int main()
{
	int a = 1;
	auto b = a;
	dereferenceable auto c = a; //invalid,int 不滿足 dereferenceable
	dereferenceable auto d = &a;
}
           

三、constraints

一個限制是一系列特化了模闆參數要求的操作及操作數。它們可以直接出現在 requires 表達式中并且直接作為 concept 的一部分。

1、Conjunctions

我們可以使用 && 連接配接兩個限制,用以表示與限制:

template <class T>
concept SignedIntegral = std::is_integral<T>::value && std::is_signed<T>::value;
           

類似邏輯表達式,這個表達式的評估是自左而右且短路的。

2、Disjunctions

類似地,我們使用 || 連接配接兩個限制,用以表示或限制:

template <class T = void>
    requires EqualityComparable<T> || Same<T, void>
struct equal_to;
           

類似邏輯或表達式,同樣是自左而右短路求值的。

3、Atomic constraints

原子限制由表達式 E 以及從 E 中出現的模闆參數到限制實體中出現的模闆參數的映射(也被稱為參數映射)。原子操作在限制标準化時構成。E 永遠不會是一個邏輯與或邏輯或的表達式(即前面提到的兩種限制)。

原子限制的滿足性檢驗是通過在 E 中替換參數映射和模闆參數完成的。如果替換的結果是一個不合法的類型或表達式,則限制不被滿足。否則,将E 從左值轉化為右值後,應該是一個傳回值類型為 bool 的 prvalue 常量表達式。當且僅當此表達式結果為 true 時,限制被滿足。注意限制判定過程中不會對表達式傳回值進行隐式類型轉換。

template<typename T>
struct S {
    constexpr operator bool() const { return true; }
};
 
template<typename T>
    requires (S<T>{})
void f(T); // invalid
           

盡管 S<int>{} 表達式支援向 bool 類型的隐式轉換,但是在限制評估過程中卻并不會考慮。是以上述代碼無法通過編譯。

當從源碼層面來說,兩個原子限制是根據相同的表達式構成的且參數映射相同時,它們被認為是完全一緻的:

#include <iostream>
using namespace std;

template<class T> constexpr bool is_meowable = true;
template<class T> constexpr bool is_cat = true;

template<class T>
concept Meowable = is_meowable<T>;

template<class T>
concept BadMeowableCat = is_meowable<T> && is_cat<T>;

template<class T>
concept GoodMeowableCat = Meowable<T> && is_cat<T>;

template<Meowable T>
void f1(T) {}

template<BadMeowableCat T>
void f1(T) {}

template<Meowable T>
void f2(T)
{
	cout << "f2(T) - Meowable" << endl;
}

template<GoodMeowableCat T>
void f2(T)
{
	cout << "f2(T) - GoodMeowableCat" << endl;
}

int main(int argc, char* argv[])
{
	f1(0); // invlaid

	f2(0);
}
           

(這部分功能可以結合後面的限制偏序看)對于 f1 模闆函數的兩個重載來說,它們是由不同的原子限制構成的,是以它們被認為是有二義性的。對于 f2 來說,GoodMeowableCat 是由 Meowable 構成的, 而且其限制性更高,是以,将 f1(0) 注釋掉後,運作結果為:

《STL源碼分析》學習筆記 — C++20 concepts一、什麼是concepts二、concepts 的定義三、constraints四、requires 子句五、requires 表達式六、requires 表達式中的 requirements 分類七、限制的偏序

4、限制标準化

限制标準化就是将一個表達式轉化為一系列由原子限制構成的 conjunctions 和 disjunctions。标準轉化包括:

a. (E) 的标準化和 E 相同

b. E1 && E2 的标準化和 E1 和 E2 的 conjunctions 相同

c. E1 || E2 的标準化和 E1 和 E2 的 disjunctions 相同

d. 對于概念 C 來說,C<A1, A2, … , AN> 的标準化和使用 A1, A2, … , AN 替換 C 中的相應參數相同。如果這樣替換之後的參數映射為無效類型或表達式,程式行為是不确定的,但是編譯時并不會産生任何問題。

template<typename T> concept A = T::value || true;
template<typename U> 
concept B = A<U*>;  // U*value || true;

template<typename V> 
concept C = B<V&>; // V&*value || true;
           

5、限制的确定

與某個聲明有關的限制是通過标準化具有如下順序操作數的邏輯與表達式決定的:

對為每個受限制模闆形參引入的限制表達式,根據其出現順序;

在模闆參數清單後面出現的requires子句中的限制表達式

對簡寫函數模闆聲明中每個擁有受限制占位符類型的形參所引入的限制表達式;

尾随requires子句中的限制表達式。

這個順序決定了當限制被執行個體化以确定是否滿足時的順序。

受限制的聲明隻能以相同的文法形式重聲明。不要求診斷。

template<Incrementable T>
void f(T) requires Decrementable<T>;
 
template<Incrementable T>
void f(T) requires Decrementable<T>; // OK, redeclaration
 
template<typename T>
    requires Incrementable<T> && Decrementable<T>
void f(T); // Ill-formed, no diagnostic required
           

四、requires 子句

關鍵字 requires 可以用于引入 requires 子句,以特化模闆參數(即可以出現在函數聲明之後,也可以在模闆參數清單之後)或函數聲明的限制:

template<typename T>
void f(T&&) requires Eq<T>; 
 
template<typename T> requires Addable<T> 
T add(T a, T b) ;
           

在此情況中,requires 後面的表達式必須是常量表達式:基礎表達式如 Swappable, std::is_integral::value 和一系列使用 && 或 || 連接配接的基礎表達式。

五、requires 表達式

requires 關鍵字同樣可以被用在 requires 表達式開頭。該表達式需要是一個 prvalue 表達式且類型為 bool (與限制的要求類似)。requires 表達式的文法如下:

requires { requirement-seq }		
requires ( parameter-list(optional) ) { requirement-seq }		
           

parameter-list 是由逗号分隔開的參數清單,與函數聲明類似,但是不支援預設參數。該清單不支援省略号(除了類模闆中用于包展開的參數)。這些參數沒有存儲持續性、連結性和生命周期,且隻能被用于協助特化要求。這些參數的作用域截止于 requirement-seq 的 }。

requirement-seq - 一系列要求,後面将介紹四種分類。

requirement 可能有賴于作用域内的模闆參數,位于 parameter-list 中的局部參數以及任何其他在封閉上下文可見的聲明。在表達式中将模闆參數替換進 requires 表達式可能會導緻不合法的類型或表達式的形成。在這種情況下,requires 表達式被評估為假而且不會導緻程式的未知行為。替換行為和語義限制檢查是按詞素順序進行的,并且在遇到可以決定 requires 表達式結果時停止。如果替換行為和語義限制檢查成功,則該表達式被評估為真。如果在requires 表達式,有一個對于所有可能的模闆參數的替換失敗行為,則程式行為未知,且沒有任何錯誤被檢測到。

六、requires 表達式中的 requirements 分類

1、簡單要求

簡單必要條件是任意不以關鍵字 requires 開頭的表達式陳述。該陳述斷言該表達式是可用的。表達式是未被評估的操作數;隻有語言的正确性被檢驗。

template<typename T>
concept Addable =
requires (T a, T b) {
    a + b; // simple requirement
};
           

2、類型要求

類型要求是跟在關鍵字 typename 後的類型名,以進行選擇限制。此要求是命名要求是合法的:這可以被用于檢驗某個确定的命名嵌套名稱存在,或是類模闆特化确定了一個類型,或是别名模闆特化确定了一個類型。一個确定了類模闆特化的類型要求不需要類型是 complete 的。

template<typename T> using Ref = T&;
template<typename T> concept C =
requires {
    typename T::inner; // required nested member name
    typename S<T>;     // required class template specialization
    typename Ref<T>;   // required alias template substitution
};
           

3、複合要求

複合要求的格式為:

其中,return-type-requirement 為類型限制,且斷言了命名表達式的屬性。替換和語義限制檢查按以下順序進行:

模闆參數被替換到表達式中;

如果 noexcept 關鍵字被使用,表達式必須不能是潛在可能抛出異常的;

如果 return-type-requirement 出現,那麼:

        模闆參數會被替換到 return-type-requirement 中;

        decltype((expression)) 必須滿足類型限制的限制。否則 require 表達式将為 false。

template<typename T> concept C2 =
requires(T x) {
    {*x} -> std::convertible_to<typename T::inner>; // 表達式 *x 必須合法
                                                    // T::inner 必須合法
                                                    // *x 必須可以轉化為T::inner
};
           

4、嵌套要求

嵌套要求的格式為:

它可以通過臨時變量的形式被用于特化額外的限制。限制表達式必須在替換模闆參數(如果有的話)之後可以被滿足。将模闆參數替換到嵌套要求中會造成限制表達式中的替換,僅限于确定是否滿足限制表達式。

template <class T>
concept Semiregular = DefaultConstructible<T> &&
    CopyConstructible<T> && Destructible<T> && CopyAssignable<T> &&
requires(T a, size_t n) {  
    requires Same<T*, decltype(&a)>;  // 嵌套要求
    { a.~T() } noexcept;  // 複合要求
    requires Same<T*, decltype(new T)>; // 嵌套要求
    requires Same<T*, decltype(new T[n])>; // 嵌套要求
    { delete new T };  // 複合要求
    { delete new T[n] }; // 複合要求
};
           

七、限制的偏序

在任何深層的分析之前,限制會被标準化,通過替換限制體内每個命名的 concept 和 requires 表達式,直到僅剩下一系列原子操作的 conjunctions 和 disjunctions。

一個限制P可以被認為歸入限制Q,如果根據P和Q中的原子限制的特征可以證明P包含Q的限制。類型和表達式不會被分析,例如: N > 0 N \gt 0 N>0 不會被歸入 N ≥ 0 N \ge 0 N≥0。

具體地,首先P被轉化為 disjunctive 标準形式,Q被轉化為conjunctive 标準形式。P歸入Q,當且僅當:

在P的分離标準形式中的每個分離的子句可以歸入Q的聯合标準形式中的每個聯合子句,其中

一個分離子句歸入一個聯合子句,當且僅當分離子句中有一個原子限制U可以歸入聯合子句中的一個原子限制V。

一個原子限制A歸入一個原子限制B,當且僅當它們完全相同的使用上述規則。

歸入規則定義了限制的部分順序,該順序用以确定:

在重載解析時,一個非模闆函數的最佳可行候選;

在重載集合中,一個非模闆函數的位址;

一個模闆中模闆參數的最佳比對;

類模闆特化的部分順序;

模闆函數的部分順序;

限制的順序如下:

如果聲明D1和D2被限制,而且D1的限制歸入D2的限制(或者D2是未被限制的),那麼D1被認為是至少與D2受到相同的限制。如果D1的限制程度不低于D2而D2的限制程度并非不低于D2。那麼相比于D2,D1就是更受限制的:

template<typename T>
concept Decrementable = requires(T t) { --t; };
template<typename T>
concept RevIterator = Decrementable<T> && requires(T t) { *t; };
 
// RevIterator 歸入 Decrementable
 
template<Decrementable T>
void f(T); // #1
 
template<RevIterator T>
void f(T); // #2 受限制更嚴格
 
void testf()
{
	f(0);       // int 隻滿足Decrementable ,選擇#1
	f((int*)0); // int* 滿足 Decrementable 和 RevIterator, 選擇 #2 因為該限制更嚴格
}
 
template<class T>
void g(T); // #3 未被限制
 
template<Decrementable T>
void g(T); // #4
 
void testg()
{
	g(true);  // bool 不滿足Decrementable,選擇#3
	g(0);     // int 滿足 Decrementable, 選擇#4因為該限制更嚴格
}
 
template<typename T>
concept RevIterator2 = requires(T t) { --t; *t; };
 
template<Decrementable T>
void h(T); // #5
 
template<RevIterator2 T>
void h(T); // #6

void testh()
{
	// h((int*)0); // 表達式中的歸入關系不會被評估,引發二義性
}
           

繼續閱讀