はじめに
C++でテンプレートパラメータを制約する方法は複数ある。制約を外れたパラメータを指定すればコンパイルエラーになる訳だが、その違いについて簡単に検証してみた。
環境
- WSL2 上で g++ を用いてコンパイル(g++のバージョンは末尾に記載)
- C++20 を使用
今回の例題
「テンプレートパラメータを4byteの算術型整数に限定する」
(例えば、uint32_t
はコンパイル通すがchar
やオブジェクト型は通さない)
以下に示すテンプレート関数でパラメータの制約を行う
template <typename T> void func()
{
T n = 0xFFFFFFFF;
std::cout << n << std::endl;
}
呼び出しは、
struct object
{
int i;
};
int main(int argc, char* argv[])
{
func<char>(); // NG: char は 1 byte
func<object>(); // NG: object はクラス型
return 0;
}
とし、不正なパラメータを指定することでどんな風にコンパイルエラーが出るかを比較してみる。
注意
- 要点のみ説明するため、
#include
文などは省略しています - コンパイルエラー内のソースコードの位置は無視して下さい
Case1 : requires を使う
template <typename T>
requires (sizeof(T) == 4 && std::is_integral_v<T>) void func()
{
Tn = 0xFFFFFFFF;
std::cout << n << std::endl;
}
test.cpp: In function ‘int main(int, char**)’:
test.cpp:58:13: error: use of function ‘void func() [with T = char]’ with unsatisfied constraints
58 | func<char>();
| ^
test.cpp:24:81: note: declared here
24 | template <typename T> requires (sizeof(T) == 4 && std::is_integral_v<T>) void func()
| ^~~~
test.cpp:24:81: note: constraints not satisfied
test.cpp: In instantiation of ‘void func() [with T = char]’:
test.cpp:58:13: required from here
test.cpp:24:81: required by the constraints of ‘template<class T> requires sizeof (T) == 4 && (is_integral_v<T>) void func()’
test.cpp:24:43: note: the expression ‘sizeof (T) == 4 [with T = char]’ evaluated to ‘false’
24 | template <typename T> requires (sizeof(T) == 4 && std::is_integral_v<T>) void func()
| ~~~~~~~~~~^~~~
test.cpp:59:15: error: use of function ‘void func() [with T = object]’ with unsatisfied constraints
59 | func<object>();
| ^
test.cpp:24:81: note: declared here
24 | template <typename T> requires (sizeof(T) == 4 && std::is_integral_v<T>) void func()
| ^~~~
test.cpp:24:81: note: constraints not satisfied
test.cpp: In instantiation of ‘void func() [with T = object]’:
test.cpp:59:15: required from here
test.cpp:24:81: required by the constraints of ‘template<class T> requires sizeof (T) == 4 && (is_integral_v<T>) void func()’
test.cpp:24:58: note: the expression ‘is_integral_v<T> [with T = object]’ evaluated to ‘false’
24 | template <typename T> requires (sizeof(T) == 4 && std::is_integral_v<T>) void func()
| ~~~~~^~~~~~~~~~~~~~~~
出力は27行。
要点を把握するためにgrep
でerror:
行のみ抽出してみる
test.cpp:58:13: error: use of function ‘void func() [with T = char]’ with unsatisfied constraints
test.cpp:59:15: error: use of function ‘void func() [with T = object]’ with unsatisfied constraints
char
やクラス型のobject
を指定しているのがまずいと教えてくれているし、どこでその不正な指定をしているかも示されている。
Case2: concept を使う
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;
}
test.cpp: In function ‘int main(int, char**)’:
test.cpp:66:13: error: use of function ‘void func() [with T = char]’ with unsatisfied constraints
66 | func<char>();
| ^
test.cpp:35:23: note: declared here
35 | template <Con T> void func()
| ^~~~
test.cpp:35:23: note: constraints not satisfied
test.cpp: In instantiation of ‘void func() [with T = char]’:
test.cpp:66:13: required from here
test.cpp:33:9: required for the satisfaction of ‘Con<T>’ [with T = char]
test.cpp:33:25: note: the expression ‘sizeof (T) == 4 [with T = char]’ evaluated to ‘false’
33 | concept Con = sizeof(T) == 4 && std::is_integral_v<T>;
| ~~~~~~~~~~^~~~
test.cpp:68:15: error: use of function ‘void func() [with T = object]’ with unsatisfied constraints
68 | func<object>();
| ^
test.cpp:35:23: note: declared here
35 | template <Con T> void func()
| ^~~~
test.cpp:35:23: note: constraints not satisfied
test.cpp: In instantiation of ‘void func() [with T = object]’:
test.cpp:68:15: required from here
test.cpp:33:9: required for the satisfaction of ‘Con<T>’ [with T = object]
test.cpp:33:40: note: the expression ‘is_integral_v<T> [with T = object]’ evaluated to ‘false’
33 | concept Con = sizeof(T) == 4 && std::is_integral_v<T>;
| ~~~~~^~~~~~~~~~~~~~~~
出力はrequires
を使った時と同じく27行、内容もほぼ同じ。
g++ --std=c++20 test.cpp |& grep error:
test.cpp:66:13: error: use of function ‘void func() [with T = char]’ with unsatisfied constraints
test.cpp:68:15: error: use of function ‘void func() [with T = object]’ with unsatisfied constraints
これもrequires
を使った時と同じ。修正すべき行が示されているので修正しやすい。
コンセプトを定義をする分少しだけ手間がかかるが、同じ制約を何度も用いるならこちらの方がよいだろう。
Case3: static_assert を使う
template <typename T> void func()
{
static_assert(sizeof(T) == 4);
static_assert(std::is_integral_v<T>);
T n = 0xFFFFFFFF;
std::cout << n << std::endl;
}
直感的な記述で簡単。
正しいコードを書けば何の問題もないのだが、不正なテンプレートパラメータを指定すると、、、
test.cpp: In instantiation of ‘void func1() [with T = char]’:
test.cpp:52:14: required from here
test.cpp:16:26: error: static assertion failed
16 | static_assert(sizeof(T) == 4);
| ~~~~~~~~~~^~~~
test.cpp:19:8: warning: overflow in conversion from ‘unsigned int’ to ‘char’ changes value from ‘4294967295’ to ‘'\37777777777'’ [-Woverflow]
19 | T n = 0xFFFFFFFF;
| ^~~~~~~~~~
test.cpp: In instantiation of ‘void func1() [with T = object]’:
test.cpp:55:16: required from here
test.cpp:17:21: error: static assertion failed
17 | static_assert(std::is_integral_v<T>);
| ~~~~~^~~~~~~~~~~~~~~~
test.cpp:19:8: error: conversion from ‘unsigned int’ to non-scalar type ‘object’ requested
19 | T n = 0xFFFFFFFF;
| ^~~~~~~~~~
test.cpp:20:12: error: no match for ‘operator<<’ (operand types are ‘std::ostream’ {aka ‘std::basic_ostream<char>’} and ‘object’)
20 | std::cout << n << std::endl;
| ~~~~~~~~~~^~~~
In file included from /usr/include/c++/10/iostream:39,
from test.cpp:1:
/usr/include/c++/10/ostream:108:7: note: candidate: ‘std::basic_ostream<_CharT, _Traits>::__ostream_type& std::basic_ostream<_CharT, _Traits>::operator<<(std::basic_ostream<_CharT, _Traits>::__ostream_type& (*)(std::basic_ostream<_CharT, _Traits>::__ostream_type&)) [with _CharT = char; _Traits = std::char_traits<char>; std::basic_ostream<_CharT, _Traits>::__ostream_type = std::basic_ostream<char>]’
108 | operator<<(__ostream_type& (*__pf)(__ostream_type&))
| ^~~~~~~~
(以下省略)
出力410行、、、 こんなの読めたもんじゃない、、、
grep
で要約してみても、
test.cpp:16:26: error: static assertion failed
test.cpp:17:21: error: static assertion failed
test.cpp:19:8: error: conversion from ‘unsigned int’ to non-scalar type ‘object’ requested
test.cpp:20:12: error: no match for ‘operator<<’ (operand types are ‘std::ostream’ {aka ‘std::basic_ostream<char>’} and ‘object’)
/usr/include/c++/10/system_error:262:5: note: candidate: ‘template<class _CharT, class _Traits> std::basic_ostream<_CharT, _Traits>& std::operator<<(std::basic_ostream<_CharT, _Traits>&, const std::error_code&)’
/usr/include/c++/10/system_error:262:5: note: template argument deduction/substitution failed:
/usr/include/c++/10/ostream:773:5: error: no type named ‘type’ in ‘struct std::enable_if<false, std::basic_ostream<char>&>’
static_assert
が失敗したのはわかるが、これだけではどこのテンプレート関数呼び出しが間違っているのかさっぱりわからない。結局膨大なエラー出力を読み解いていかないといけない。
ちなみにこのエラーでいう16~20行はfunc()
を書いている部分であり、呼び出し元ではない。
まとめ
static_assert
は文法が直感的で書くのは簡単だが、山のようにコンパイルエラーは吐いたりする。C++20から導入されたconcept
は記述の仕方にちょっとクセがあって理解しにくいが、コンパイルエラーもわかりやすくなるので、コーディングの省力化になるかな。
今回使用した g++ のバージョン情報
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.