0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-04-20

はじめに

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)
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行。多いけどまあ何とか読める。
要点を把握するためにgreperror:行のみ抽出してみる

$ 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)
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)
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++ --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.
0
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?