C++20におけるテンプレートパラメータの制約方法の比較

# はじめに
C++でテンプレートパラメータを制約する方法は複数ある。制約を外れたパラメータを指定すればコンパイルエラーになる訳だが、そのエラー出力の違いについて簡単に検証してみた。

# 環境
* WSL2 上で g++ を用いてコンパイル（g++のバージョンは末尾に記載）
* C++20 を使用

# 今回の例題
「テンプレートパラメータを4byteの算術型整数に限定する」

(例えば、```uint32_t```はコンパイル通すが```char```やオブジェクト型は通さない）


以下に示すテンプレート関数に対して、複数の方法でパラメータの制約を行う。
```C++
template <typename T> void func()
{
    T n = 0xFFFFFFFF;
    std::cout << n << std::endl;
}
```

このテンプレート関数を、わざと不正なテンプレートパラメータを指定して呼び出してみる。
```C++
struct object
{
    int i;
};
    
int main(int argc, char* argv[])
{
    func<char>();      // NG: char は 1 byte
    func<object>();    // NG: object はクラス型
    return 0;
}
```



# Case1 : requires を使う
```C++
template <typename T>
requires (sizeof(T) == 4 && std::is_integral_v<T>)
void func()
{
    Tn = 0xFFFFFFFF;
    std::cout << n << std::endl;
}
```
<details><summary>コンパイル用ソースコード(test1.cpp)</summary>

```c++:test1.cpp
#include <iostream>
#include <type_traits>

template <typename T>
requires (sizeof(T) == 4 && std::is_integral_v<T>)
void func()
{
    T n = 0xFFFFFFFF;
    std::cout << n << std::endl;
}

struct object
{
    int i;
};

int main(int argc, char* argv[])
{
    func<char>();
    func<object>();
    return 0;
}
```
</details>

```linux:コンパイル結果
test1.cpp: In function ‘int main(int, char**)’:
test1.cpp:19:15: error: no matching function for call to ‘func<char>()’
   19 |     func<char>();
      |     ~~~~~~~~~~^~
test1.cpp:5:6: note: candidate: ‘template<class T>  requires  sizeof (T) == 4 && (is_integral_v<T>) void func()’
    5 | void func()
      |      ^~~~
test1.cpp:5:6: note:   template argument deduction/substitution failed:
test1.cpp:5:6: note: constraints not satisfied
test1.cpp: In substitution of ‘template<class T>  requires  sizeof (T) == 4 && (is_integral_v<T>) void func() [with T = char]’:
test1.cpp:19:15:   required from here
test1.cpp:5:6:   required by the constraints of ‘template<class T>  requires  sizeof (T) == 4 && (is_integral_v<T>) void func()’
test1.cpp:4:21: note: the expression ‘sizeof (T) == 4 [with T = char]’ evaluated to ‘false’
    4 | requires (sizeof(T) == 4 && std::is_integral_v<T>)
      |           ~~~~~~~~~~^~~~
test1.cpp:20:17: error: no matching function for call to ‘func<object>()’
   20 |     func<object>();
      |     ~~~~~~~~~~~~^~
test1.cpp:5:6: note: candidate: ‘template<class T>  requires  sizeof (T) == 4 && (is_integral_v<T>) void func()’
    5 | void func()
      |      ^~~~
test1.cpp:5:6: note:   template argument deduction/substitution failed:
test1.cpp:5:6: note: constraints not satisfied
test1.cpp: In substitution of ‘template<class T>  requires  sizeof (T) == 4 && (is_integral_v<T>) void func() [with T = object]’:
test1.cpp:20:17:   required from here
test1.cpp:5:6:   required by the constraints of ‘template<class T>  requires  sizeof (T) == 4 && (is_integral_v<T>) void func()’
test1.cpp:4:34: note: the expression ‘is_integral_v<T> [with T = object]’ evaluated to ‘false’
    4 | requires (sizeof(T) == 4 && std::is_integral_v<T>)
      |                             ~~~~~^~~~~~~~~~~~~~~~
```

出力は29行。多いけどまあ何とか読める。
要点を把握するために```grep```で```error:```行のみ抽出してみる
```linux
$ g++ --std=c++20 test1.cpp |& grep error:
test1.cpp:19:15: error: no matching function for call to ‘func<char>()’
test1.cpp:20:17: error: no matching function for call to ‘func<object>()’
```
```char```やクラス型の```object```を指定しているのが不正だと教えてくれている。この19, 20行目というのは、ちょうど```main```関数内で不正な```func```関数を呼び出している行であり、**修正すべき場所がすぐわかる**。


# Case2: concept を使う
```C++
template <typename T>
concept Con = sizeof(T) == 4 && std::is_integral_v<T>;

template <Con T> void func()
{
    Tn=0xFFFFFFFF;
    std::cout << n << std::endl;
}
```
<details><summary>コンパイル用ソースコード(test2.cpp)</summary>

```c++:test2.cpp
#include <iostream>
#include <type_traits>

template <typename T>
concept Con = sizeof(T) == 4 && std::is_integral_v<T>;

template <Cont T> void func()
{
    T n = 0xFFFFFFFF;
    std::cout << n << std::endl;
}

struct object
{
    int i;
};

int main(int argc, char* argv[])
{
    func<char>();
    func<object>();
    return 0;
}
```
</details>


```linux:コンパイル結果
test2.cpp: In function ‘int main(int, char**)’:
test2.cpp:20:15: error: no matching function for call to ‘func<char>()’
   20 |     func<char>();
      |     ~~~~~~~~~~^~
test2.cpp:6:23: note: candidate: ‘template<class T>  requires  Con<T> void func()’
    6 | template <Con T> void func()
      |                       ^~~~
test2.cpp:6:23: note:   template argument deduction/substitution failed:
test2.cpp:6:23: note: constraints not satisfied
test2.cpp: In substitution of ‘template<class T>  requires  Con<T> void func() [with T = char]’:
test2.cpp:20:15:   required from here
test2.cpp:4:9:   required for the satisfaction of ‘Con<T>’ [with T = char]
test2.cpp:4:25: note: the expression ‘sizeof (T) == 4 [with T = char]’ evaluated to ‘false’
    4 | concept Con = sizeof(T) == 4 && std::is_integral_v<T>;
      |               ~~~~~~~~~~^~~~
test2.cpp:21:17: error: no matching function for call to ‘func<object>()’
   21 |     func<object>();
      |     ~~~~~~~~~~~~^~
test2.cpp:6:23: note: candidate: ‘template<class T>  requires  Con<T> void func()’
    6 | template <Con T> void func()
      |                       ^~~~
test2.cpp:6:23: note:   template argument deduction/substitution failed:
test2.cpp:6:23: note: constraints not satisfied
test2.cpp: In substitution of ‘template<class T>  requires  Con<T> void func() [with T = object]’:
test2.cpp:21:17:   required from here
test2.cpp:4:9:   required for the satisfaction of ‘Con<T>’ [with T = object]
test2.cpp:4:38: note: the expression ‘is_integral_v<T> [with T = object]’ evaluated to ‘false’
    4 | concept Con = sizeof(T) == 4 && std::is_integral_v<T>;
      |                                 ~~~~~^~~~~~~~~~~~~~~~
```
こちらも29行。内容は```requires```を使った時とほぼ同じ。
要約してみると、
```linux
$ g++ --std=c++20 test2.cpp |& grep error:
test2.cpp:20:15: error: no matching function for call to ‘func<char>()’
test2.cpp:21:17: error: no matching function for call to ‘func<object>()’
```

修正しないといけない行が示されているのでわかりやすい。

コンセプトを定義をする分少しだけ手間がかかるが、同じ制約を何度も用いるならこちらの方が効率的。


# Case3: static_assert を使う
```C++
template <typename T> void func()
{
    static_assert(sizeof(T) == 4, "Variable size is not 4 byte");
    static_assert(std::is_integral_v<T>, "Variable is not an integral type");
    T n = 0xFFFFFFFF;
    std::cout << n << std::endl;
}
```
直感的な記述で簡単。
正しいコードを書けば何の問題もないのだが、不正なテンプレートパラメータを指定すると、どうなるか？
<details><summary>コンパイル用ソースコード(test3.cpp)</summary>

```c++:test3.cpp
#include <iostream>
#include <type_traits>

template <typename T> void func()
{
    static_assert(sizeof(T) == 4, "Variant size is not 4 byte");
    static_assert(std::is_integral_v<T>, "Variant is not integral");
    T n = 0xFFFFFFFF;
    std::cout << n << std::endl;
}

struct object
{
    int i;
};

int main(int argc, char* argv[])
{
    func<char>();
    func<object>();
    return 0;
}
```
</details>

```linux:コンパイル結果
test3.cpp: In instantiation of ‘void func() [with T = char]’:
test3.cpp:19:15:   required from here
test3.cpp:5:29: error: static assertion failed: Variant size is not 4 byte
    5 |     static_assert(sizeof(T) == 4, "Variant size is not 4 byte");
      |                   ~~~~~~~~~~^~~~
test3.cpp:5:29: note: the comparison reduces to ‘(1 == 4)’
test3.cpp:7:11: warning: overflow in conversion from ‘unsigned int’ to ‘char’ changes value from ‘4294967295’ to ‘'\37777777777'’ [-Woverflow]
    7 |     T n = 0xFFFFFFFF;
      |           ^~~~~~~~~~
test3.cpp: In instantiation of ‘void func() [with T = object]’:
test3.cpp:20:17:   required from here
test3.cpp:6:24: error: static assertion failed: Variant is not integral
    6 |     static_assert(std::is_integral_v<T>, "Variant is not integral");
      |                   ~~~~~^~~~~~~~~~~~~~~~
test3.cpp:6:24: note: ‘std::is_integral_v<object>’ evaluates to false
test3.cpp:7:11: error: conversion from ‘unsigned int’ to non-scalar type ‘object’ requested
    7 |     T n = 0xFFFFFFFF;
      |           ^~~~~~~~~~
test3.cpp:8:15: error: no match for ‘operator<<’ (operand types are ‘std::ostream’ {aka ‘std::basic_ostream<char>’} and ‘object’)
    8 |     std::cout << n << std::endl;
      |     ~~~~~~~~~~^~~~
In file included from /usr/include/c++/12/iostream:39,
                 from test3.cpp:1:
/usr/include/c++/12/ostream:108:7: note: candidate: ‘std::basic_ostream<_CharT, _Traits>::__ostream_type& std::basic_ostream<_CharT, _Traits>::operator<<(__ostream_type& (*)(__ostream_type&)) [with _CharT = char; _Traits = std::char_traits<char>; __ostream_type = std::basic_ostream<char>]’
  108 |       operator<<(__ostream_type& (*__pf)(__ostream_type&))
      |       ^~~~~~~~
/usr/include/c++/12/ostream:108:36: note:   no known conversion for argument 1 from ‘object’ to ‘std::basic_ostream<char>::__ostream_type& (*)(std::basic_ostream<char>::__ostream_type&)’ {aka ‘std::basic_ostream<char>& (*)(std::basic_ostream<char>&)’}
  108 |       operator<<(__ostream_type& (*__pf)(__ostream_type&))
      |                  ~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/12/ostream:117:7: note: candidate: ‘std::basic_ostream<_CharT, _Traits>::__ostream_type& std::basic_ostream<_CharT, _Traits>::operator<<(__ios_type& (*)(__ios_type&)) [with _CharT = char; _Traits = std::char_traits<char>; __ostream_type = std::basic_ostream<char>; __ios_type = std::basic_ios<char>]’
  117 |       operator<<(__ios_type& (*__pf)(__ios_type&))
      |       ^~~~~~~~
/usr/include/c++/12/ostream:117:32: note:   no known conversion for argument 1 from ‘object’ to ‘std::basic_ostream<char>::__ios_type& (*)(std::basic_ostream<char>::__ios_type&)’ {aka ‘std::basic_ios<char>& (*)(std::basic_ios<char>&)’}
  117 |       operator<<(__ios_type& (*__pf)(__ios_type&))
      |                  ~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~

（以下省略）
```
出力345行。これは読む気になれないが、テンプレートプログラミングの宿命。
エラー行だけ抽出してみる。
```linux
$ g++ --std=c++20 test3.cpp |& grep error:
test3.cpp:5:29: error: static assertion failed: Variant size is not 4 byte
test3.cpp:6:24: error: static assertion failed: Variant is not integral
test3.cpp:7:11: error: conversion from ‘unsigned int’ to non-scalar type ‘object’ requested
test3.cpp:8:15: error: no match for ‘operator<<’ (operand types are ‘std::ostream’ {aka ‘std::basic_ostream<char>’} and ‘object’)
/usr/include/c++/12/system_error:279:5: note: candidate: ‘template<class _CharT, class _Traits> std::basic_ostream<_CharT, _Traits>& std::operator<<(basic_ostream<_CharT, _Traits>&, const error_code&)’
/usr/include/c++/12/system_error:279:5: note:   template argument deduction/substitution failed:
/usr/include/c++/12/ostream:754:5: error: template constraint failure for ‘template<class _Os, class _Tp>  requires (__derived_from_ios_base<_Os>) && requires(_Os& __os, const _Tp& __t) {__os << __t;} using __rvalue_stream_insertion_t = _Os&&’
```

```static_assert```が失敗したのはわかる。理由もわかる。
だが、ここで示されている5, 6行目というのは、```static_assert```を書いている行（パラメータの制約をしている場所）であり、**不正な呼び出しを行った場所ではない**。つまり、**ソースコードのどこを修正すればよいか、このエラー出力ではわからない**ということ。自分で不正な```func```関数呼び出しを見つけて修正しないといけないのだが、プログラムが大きく複雑になってくると大変。

# まとめ
```static_assert```は文法が直感的で書くのは簡単だが、山のようにコンパイルエラーを吐いたりする。C++20から導入された```concept```は記述の仕方にちょっとクセがあって理解しにくいが、コンパイルエラーもわかりやすくなるので、コーディングの省力化になるのかな。


# 今回使用した g++ のバージョン情報
```linux:g++ --version
g++ (Debian 10.2.1-6) 10.2.1 20210110
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
```