0%

Concept in C++20

Why Concepts?

實例分析

Function overload by type

當我們需要一個函數toString,傳入一個參數,Pseudo Code大概是這樣

1
2
3
4
5
6
7
8
9
template <typename T>
std::string toString(const T& input)
{
if (input have toString member function) {
return input.toString();
} else {
return "unknown format";
}
}

該怎麼做

C++98 Way

看看就好, 這手法現在也不需要理解了, 太舊了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
template <typename T>
class has_toString {
private:
typedef char Yes;
typedef Yes No[2];

template <typename U, U> struct really_has;

template <typename C> static Yes& Test(really_has <std::string(C::*)() const,
&C::toString>*);

template <typename> static No& Test(...);

public:
static bool const value = sizeof(Test<T>(0)) == sizeof(Yes);
};

template<bool B, class T = void> // Default template version.
struct enable_if {}; // This struct doesn't define "type" and the substitution will fail if you try to access it.

template<class T> // A specialisation used if the expression is true.
struct enable_if<true, T> { typedef T type; }; // This struct do have a "type" and won't fail on access.

template <typename T>
std::string toString(const T& input, typename enable_if<has_toString<T>::value, int>::type t = 0)
{
return input.toString();
}

template <typename T>
std::string toString(const T& input, typename enable_if<!has_toString<T>::value, int>::type t = 0)
{
return "unknown format";
}

我想你不會懷念他的
幾個缺點
– 沒有type_traits,所以enable_if之類的要自己寫
– 每增加一個Signature偵測,就要多個類似has_toString的東西出現
– Function overload的signagure要修改
– 奇醜無比

C++11 Way

僅列出跟C++98不同的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
template <typename T>
class has_toString {
private:
typedef char Yes;
typedef Yes No[2];

template<typename C> static auto Test(void*)
-> decltype(std::string{ std::declval<C const>().toString() }, Yes{});

template<typename> static No& Test(...);

public:
static bool const value = sizeof(Test<T>(0)) == sizeof(Yes);
};

template <typename T, typename std::enable_if<has_toString<T>::value, int>::type t = 0>
std::string toString(const T& input)
{
return input.toString();
}

template <typename T, typename std::enable_if<!has_toString<T>::value, int>::type t = 0>
std::string toString(const T& input)
{
return "unknown format";
}

基本上差不了多少,不過現在不用修改toString的Signature,順眼多了
C++14之後的更新基本上不可用,MSVC不支援,就不列出了

Boost Hana Solution

1
2
3
4
5
6
7
8
9
10
auto has_toString = boost::hana::is_valid([](auto&& obj) -> decltype(obj.toString()) { });

template <typename T>
std::string toString(T const& obj)
{
return boost::hana::if_(has_toString(obj),
[] (auto& x) { return x.toString(); },
[] (auto& x) { return "unknown format"; }
)(obj);
}

這基本上就很接近我們的Pseudo code,不過還要額外的Hana dependency

C++17

C++14之後的更新基本上不可用,MSVC不支援,就不列出了
不過我們可以用if constexpr把兩個 toString合併成一個

Think more

我們只是檢查一個條件toString,如果有復合的條件約束的話該怎麼辦, 假設我們還要檢查一個ver的member variable或者這個typeu定義了一個value_type,上面的方式就變得無力

C++ 20 Concept Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename T>
concept has_toString = requires(const T &t) {
typename T::value_type;
{ t.ver } -> int;
{ t.toString() }->std::same_as<std::string>;
};

template <typename T>
std::string toString(const T& input)
{
if constexpr (has_toString<T>)
return input.toString();
else
return "unknown format";
}

配合C++17的if constexpr,很漂亮的解決了上述的難題

Debug Template Code

在寫Generic template code的時候,往往要跟Error message打交道
有了Concept約束之後,比較容易觀測出到底哪裡錯了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
}

錯誤訊息如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<source>:2:1: error: 'concept' does not name a type
2 | concept have_value = requires(T a) {
| ^~~~~~~

<source>:2:1: note: 'concept' only available with '-fconcepts'
<source>:7:1: error: 'requires' does not name a type
7 | requires have_value<T>
| ^~~~~~~~

<source>: In function 'int main()':
<source>:18:12: error: 'get_value' was not declared in this scope
18 | auto y = get_value(b);
| ^~~~~~~~~

Compiler returned: 1

很明顯的我們的Type不符合Concept的要求
這個範例簡單到就算用原先的方式,有經驗的Porgrammer一樣能夠找出錯誤
如果你的Code是一層一層的Template累加起來,就知道這東西多有用了

How to use Concept

Four Ways

Requires Clause

1
2
3
template <typename T>
requires std::integral<T>
void log(T&& x);

Trailing Requires Clause

1
2
template <typename T>
void log(T&& x) requires std::integral<T>

雖然比起第一種方式顯得冗長,不過有種特異功能第一種方式做不到

1
2
template <typename T>
void log(T&& x) requires std::integral<decltype(x)>

由於Trailing Requires Clause可以看到整個Function signature, 因此可以使用decltype做處理

Constrained Template Parameters

1
2
template <std::integral T>
void log(T&& x)

很顯然的, T這個類型必須滿足std::integral的約束
這方案還有一種好處,在不支援C++20的Compilier,只要把std::integral換成typename
就能照常編譯了,不過享受不到Concept的好處
不過可以在C++20檢查過之後Poring回Pre-C++20的Compiler

Abbreviated function template

顧名思義,這種方式只能在 Function template上用,是一種語法糖
看看以下這個案例

1
2
3
4
Arithmetic auto sub(Arithmetic auto fir, Arithmetic auto sec) 
{
return fir - sec;
}

基本上等價於Constrained Template Parameters

1
2
3
4
5
template <Arithmetic T, Arithmetic T2>
auto sub(T fir, T2 sec)
{
return fir - sec;
}

比起上面的寫法,連template宣告都省下來了,不過上面阿寫法可以蛻化為Pre-C++20,這不行

Function overload

Concept允許function overload,比起一般的template function, 通過concept檢驗的function會優先採用
例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void overload(auto t){
std::cout << "auto : " << t << std::endl;
}

template <typename T>
requires std::is_integral_v<T>
void overload(T t){
std::cout << "Integral : " << t << std::endl;
}

void overload(long t){
std::cout << "long : " << t << std::endl;
}

int main(){

std::cout << std::endl;

overload(3.14); // (1)
overload(2010); // (2)
overload(2020l); // (3)

std::cout << std::endl;

}

Customize Concept

The simplest concept

Concept就是Compile-time的boolean expression

1
2
template <typename T>
concept superfluous = true;

既然是個boolean expression,就能做所有boolean operator支援的操作

1
2
3
4
template <typename T>
concept integral = std::is_integral_v<T>;
template <typename T>
concept number = std::integral<T> || std::floating_point<T>;

More complext concept

定義一個能支援遞增的Concept

1
2
3
4
5
template <typename T>
concept you_can_increment_it = requires(T x)
{
{++x};
};

定義能夠進行四則運算的Concept, 注意這邊需要兩個Type

1
2
3
4
5
6
7
8
template <typename X, typename Y>
concept they_are_mathsy = requires(X x, Y y)
{
{ x * y };
{ x / y };
{ x + y };
{ x - y };
};

對Return type有要求的Concept

1
2
3
4
5
6
template <typename T>
concept you_can_increment_it = requires(T x)
{
{++x} -> std::same_as<T>;
{x + x} -> std::convertible_to<int>;
};

檢查type是否有member structure和member function

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T>
concept its_a_dragon = requires(T x)
{
// this type trait must be able to be instantiated
typename dragon_traits<T>;

// T has a nested typename for some pointer idk use your imagination
typename T::dragon_clan_ptr;

{x.breathe_fire()};
{x.dragon_breath_firepower()} -> std::convertible_to<uint>;
};

Concept all the thing

當然,你也可以在其他地方加上Concept

1
Integral auto integ = getIntegral(10);

你的return value自然就要符合Integral的約束,幫助我們在Code Maintenance和Refactoring中得到優勢

Conclusion

比起C++Module和C++Coroutine改變遊戲規則的重要性比起來,Conceptˋ只能算是錦上添花,不過對Library developer來說還是有用的

Reference

Is it possible to write a template to check for a function’s existence?
Notes on C++ SFINAE
An introduction to C++’s SFINAE concept: compile-time introspection of a class member
Substitution Failure is Error and Not An Error.
C++20: Concepts, the Details
C++20 Concepts
C++20: Define Concepts