はじめに
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;
}
Case1 : requires を使う
template <typename T>
requires (sizeof(T) == 4 && std::is_integral_v<T>)
void func()
{
Tn = 0xFFFFFFFF;
std::cout << n << std::endl;
}
コンパイル用ソースコード(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;
}
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:
行のみ抽出してみる
$ 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 を使う
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;
}
コンパイル用ソースコード(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;
}
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
を使った時とほぼ同じ。
要約してみると、
$ 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 を使う
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;
}
直感的な記述で簡単。
正しいコードを書けば何の問題もないのだが、不正なテンプレートパラメータを指定すると、どうなるか?
コンパイル用ソースコード(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;
}
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行。これは読む気になれないが、テンプレートプログラミングの宿命。
エラー行だけ抽出してみる。
$ 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++ のバージョン情報
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.