C++20にてついに導入されたコンセプト、書け方にムラがあるので少し整理してみます。
1. typename
の代わりに
まず、従来template<typename T>
と書いていたところのtypename
の代わりにコンセプト名を書けます。
#include <concepts> #include <iostream> //環であるか? template<typename T> concept ring = requires(T a, T b) { {a + b} -> std::convertible_to<T>; {a * b} -> std::convertible_to<T>; }; template<ring T> void f(T&&) { std::cout << "T is ring" << std::endl; } template<typename T> void f(T&&) { std::cout << "T is not ring" << std::endl; } //足し算だけはある型 struct S { S operator+(S); }; int main() { f(10); f(1.0); f(S{}); f(std::cout); } /* 出力 T is ring T is ring T is not ring T is not ring */
ring
というコンセプトによって型が環であるかどうかを雑に判定しています。使用しているのは関数f()
の1つ目のオーバーロードです。
template<ring T> void f(T&&);
従来typename
かclass
を書いていた所にコンセプトを書けます。分かりやすいので基本的にはこう書かれることが多いのではないかと思います。
標準ライブラリでコンセプトが使用される時も基本的にこの形で書かれています。
クラスの場合は次のようになります。
#include <concepts> #include <iostream> template<typename T> concept ring = requires(T a, T b) { {a + b} -> std::convertible_to<T>; {a * b} -> std::convertible_to<T>; }; template<typename T> struct wrap { static constexpr char Struct[] = "T is not ring"; }; template<ring T> struct wrap<T> { static constexpr char Struct[] = "T is ring"; }; struct S { S operator+(S); }; int main() { std::cout << wrap<int>::Struct << std::endl; std::cout << wrap<S>::Struct << std::endl; } /* 出力 T is ring T is not ring */
クラスの場合はオーバーロード?するのに部分特殊化を利用する必要がありますが、基本的には同じように書くことができます。
ただし、この方法では1つの型に1つの制約しかかけられません。制約を追加したい場合は新しくコンセプト定義を書かなければなりません。
//環かつデフォルト構築可能と言う制約をかけようとしたができない・・・ template<ring && std::default_constructible T> void f(T&&);
- メリット
- 宣言が複雑にならない(相対的に)
- デメリット
- 1つの型に1つの制約しかかけられない
以降、このf()
とwrap
だけを見ていくことにします。
2. 前置requires
節
requires
節というものを利用してテンプレートパラメータの宣言のすぐ後に制約を書きます。
//関数 template<typename T> requires ring<T> void f(T&&); //クラス template<typename T> requires ring<T> struct wrap<T>;
この形では追加の制約をかけたいときに簡単に書くことができます。
//デフォルト構築可能(std::default_constructible)と言う制約を追加する //関数 template<typename T> requires ring<T> && std::default_constructible<T> void f(T&&); //クラス template<typename T> requires ring<T> && std::default_constructible<T> struct wrap<T>;
- メリット
&&
や||
で繋いで複数の制約をかけられる
- デメリット
- 関数宣言が複雑になりうる
3. 後置requires
節 ※関数のみ
先ほどのrequires
節、関数の後ろにも置けます。
//関数 template<typename T> void f(T&&) requires ring<T>;
ただし、クラスではこの書き方はできません。関数のみになります。
この書き方ではクラステンプレートの非テンプレートメンバ関数に対して制約をかけることができます。
template<typename T> struct wrap { T t; //Tが環である時のみfma()を使用可能 T fma(T x) requires ring<T> { return t * x + t; } }; struct S { S operator+(S); }; int main() { wrap<int> wi{10}; wrap<S> si{}; std::cout << wi.fma(20) << std::endl; //ok、210 si.fma({}); //コンパイルエラー }
この様な非テンプレート関数に制約をかけるにはこの書き方をするしかありません。
- メリット
&&
や||
で繋いで複数の制約をかけられる- 非テンプレート関数に制約をかけられる
- デメリット
- 関数宣言が複雑になりうる
- クラスで使えない
4. auto
による簡略構文 ※関数のみ
C++20より通常の関数でもジェネリックラムダのようにauto
を使って引数を宣言できます。その際にコンセプトを添えることで制約をかけることができます。
void f(ring auto&&);
- メリット
- 記述量が減る
- 引数と制約の対応が分かりやすい
- デメリット
- 1つの型に1つの制約しかかけられない
- クラスで使えない
- 非型テンプレートパラメータの時は使用可能
5. 1と2(or 3)
1の書き方に2の書き方を合わせて書くことで、基本制約+追加の制約みたいな気持ちを込めて書くことができます。標準ライブラリではこの形式で制約されていることが多いようです。
//環でありデフォルト構築可能、という制約をする //関数 template<ring T> requires std::default_constructible<T> void f(T&&); //クラス template<ring T> requires std::default_constructible<T> struct wrap<T>;
クラスでは使えませんがこの時に3の方法(関数名の後)で書いても大丈夫です。
- メリット
- 複数の制約を少しすっきりと書ける
- 基本+追加のような意味を込められる
- デメリット
- 1つの型に対する制約が散らばるので見辛くなりうる
4と3
一応書けますよ、という・・・
void f(ring auto&& x) requires std::default_constructible<decltype(x)>;
あえてこういう書き方をしたいことがあるのか、わかりません・・・
全部盛り
2つと言わずに全部使っても構いませんよ、もちろん!
//関数 template<ring T> requires ring<T> void f(T&&) requires ring<T>;
参考文献
謝辞
この記事の9割は以下の方によるご指摘によって成り立っています