[C++]コンセプトの5景

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
*/

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

ringというコンセプトによって型が環であるかどうかを雑に判定しています。使用しているのは関数f()の1つ目のオーバーロードです。

template<ring T>
void f(T&&);

従来typenameclassを書いていた所にコンセプトを書けます。分かりやすいので基本的にはこう書かれることが多いのではないかと思います。
標準ライブラリでコンセプトが使用される時も基本的にこの形で書かれています。

クラスの場合は次のようになります。

#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
*/

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

クラスの場合はオーバーロード?するのに部分特殊化を利用する必要がありますが、基本的には同じように書くことができます。

ただし、この方法では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>;

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

この形では追加の制約をかけたいときに簡単に書くことができます。

//デフォルト構築可能(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>;

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

ただし、クラスではこの書き方はできません。関数のみになります。

この書き方ではクラステンプレートの非テンプレートメンバ関数に対して制約をかけることができます。

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({});   //コンパイルエラー
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

この様な非テンプレート関数に制約をかけるにはこの書き方をするしかありません。

  • メリット
    • &&||で繋いで複数の制約をかけられる
    • 非テンプレート関数に制約をかけられる
  • デメリット
    • 関数宣言が複雑になりうる
    • クラスで使えない

4. autoによる簡略構文 ※関数のみ

C++20より通常の関数でもジェネリックラムダのようにautoを使って引数を宣言できます。その際にコンセプトを添えることで制約をかけることができます。

void f(ring auto&&);

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

  • メリット
    • 記述量が減る
    • 引数と制約の対応が分かりやすい
  • デメリット
    • 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>;

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

クラスでは使えませんがこの時に3の方法(関数名の後)で書いても大丈夫です。

  • メリット
    • 複数の制約を少しすっきりと書ける
    • 基本+追加のような意味を込められる
  • デメリット
    • 1つの型に対する制約が散らばるので見辛くなりうる

4と3

一応書けますよ、という・・・

void f(ring auto&& x) requires std::default_constructible<decltype(x)>;

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

あえてこういう書き方をしたいことがあるのか、わかりません・・・

全部盛り

2つと言わずに全部使っても構いませんよ、もちろん!

//関数
template<ring T>
requires ring<T>
void f(T&&) requires ring<T>;

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

参考文献

謝辞

この記事の9割は以下の方によるご指摘によって成り立っています

この記事のMarkdownソース