C++20 では全C++プログラマ待望[要出展]の「コンセプト(Concepts)」が正式導入されました。
この記事では“入門以前”と称して、C++コンセプトのメリットをざっくりと説明します。詳細な仕様説明は行わないことを予めご了承ください。
後日追記: 2020-12-15付けで国際標準規格 ISO/IEC 14882:2020 としてC++20が正式発行されました。
C++コンセプトって何?
例えば cppreference.com(日本語版) 冒頭では下記のように説明されます。なるほど分からん。言語仕様の説明としては間違いありませんが、これだけでC++コンセプトを理解するのはさすがに難しいでしょう。
クラステンプレート、関数テンプレート、および非テンプレート関数 (一般的にはクラステンプレートのメンバ) は、制約と紐付けることができます。 制約はテンプレート引数に対する制限を指定します。 これは最も適切な関数オーバーロードおよびテンプレート特殊化を選択するために使用することができます。
そのような要件の名前付き集合はコンセプトと呼ばれます。 それぞれのコンセプトは述語であり、コンパイル時に評価され、制約として使用されたテンプレートのインタフェースの一部になります。
また cpprefjp - C++日本語リファレンス では次のように要約しています。これなら分かりやすいでしょうか?
C++20から導入される「コンセプト (concepts)」は、テンプレートパラメータを制約する機能である。
具象と抽象のあいだ
C++コンセプトの “機能” を一言で表すと、cpprefjpにある通り「テンプレートパラメータを 制約する」ものに過ぎません。テンプレート(template)は昔からあるC++言語機能ですから、テンプレートパラメータを制約するコンセプトは「C++プログラムが表現できることを制限する機能」という見方もできます。
古き良き(非テンプレートな)関数では、引数や戻り値の型(type)を具体的に特定したうえで処理を記述します。一方、C++テンプレート関数では任意の型に対する処理を汎用的に記述できます。例えば整数型に対する操作proc
を、通常の関数とテンプレート関数それぞれで記述すると:
// 通常の関数
int proc(int n);
long proc(long n);
/*...(オーバーロード多数)...*/
// 関数テンプレート
template <typename T>
T proc(T n);
通常の関数では 型情報が具象的すぎる ため、操作対象の型ごとに関数オーバーロードを定義していく必要があります。一方の汎化されたテンプレート関数は あらゆる型を受け付けてしまう ため、想定外の型を与えると無意味な結果を返したり難解なコンパイルエラーを引き起こします。
C++20コンセプトを利用すると、具象的すぎる通常の関数オーバーロード定義と抽象的すぎるテンプレート関数定義のバランスをとった程よい抽象度、つまり 特定の要件を満たす型ファミリに対してのみ汎化された操作 を定義できます。
#include <concepts>
// コンセプト制約付き関数テンプレート
template <std::integral T> // Tは整数型
T proc(T n);
さよなら、SFINAE。ようこそ、コンセプト制約。
ここからは架空のシナリオを通して、旧来C++テンプレートの汎化能力と問題点、新機能C++20コンセプトの利点をみていきます。
(この物語はフィクションであり、実在の人物・団体とは一切関係ありません。)
Day1: 普通の関数
C++プログラマであるあなたに、下記仕様を満たす関数の作成依頼がありました。
「与えられた数値を2倍して表示する関数twice
が欲しいなぁ...」
なめとんのか うーん難しい課題ですね...でもなんとかやってみましょう。
#include <iostream>
void twice(int x) {
std::cout << (x * 2) << std::endl;
}
Day2: 関数オーバーロード
「昨日のtwice
関数だけど、小数を入力したときの動きがヘンなんだけど。」
twice(21); // 42を出力
twice(3.14); // 6を出力(6.28を期待)
なるほど、とりあえず修正しときました。
void twice(int x) {
std::cout << (x * 2) << std::endl;
}
void twice(double x) {
std::cout << (x * 2) << std::endl;
}
Day3: 関数テンプレート
昨日は整数型(int
)と浮動小数点数型(double
)の関数オーバーロードでアドホックに対応してしまいましたが、関数テンプレートを使えば 異なる型に対しても内部実装を共通化 できます。
template <typename T>
void twice(T x) {
std::cout << (x * 2) << std::endl;
}
これこそが ジェネリックプログラミング(generic programming) のチカラ。
Day4: 関数テンプレート×SFINAE(1)
「例のtwice
関数だけど、数値以外では*
って出力したいんだよね。よろしく。」
(# ^ω^)
#include <iostream>
#include <type_traits>
// 数値型(整数or浮動小数点数)を判定するメタ関数
template <typename T>
struct is_number : std::integral_constant<bool,
std::is_integral<T>::value || std::is_floating_point<T>::value
> {};
// #1 数値型以外に対して有効となる関数テンプレート
template <typename T>
typename std::enable_if<!is_number<T>::value>::type
twice(T) {
std::cout << "*" << std::endl;
}
// #2 数値型に対して有効となる関数テンプレート
template <typename T>
typename std::enable_if<is_number<T>::value>::type
twice(T x) {
std::cout << (x * 2) << std::endl;
}
現行C++の テンプレートメタプログラミング でテンプレートパラメータによる分岐制御を実現するには、 SFINAE (Substitution Failure Is Not An Error) とよばれる 技巧的なテクニックが必要とされます。1
Day5: 関数テンプレート×SFINAE(2)
「実はさぁ、符号なし(unsigned)型に限って末尾u
を出力するらしい。月曜までによろしく。」
twice(21); // 42を出力
twice(100u); // 200uを出力
twice(3.14); // 6.28を出力
twice("X"); // *を出力
(メ゚皿゚) そういう要件は先に言ってください。
#include <iostream>
#include <type_traits>
// 数値型(整数or浮動小数点数)を判定するメタ関数
template <typename T>
struct is_number : std::integral_constant<bool,
std::is_integral<T>::value || std::is_floating_point<T>::value
> {};
// #1 数値型以外に対して有効となる関数テンプレート
template <typename T>
typename std::enable_if<!is_number<T>::value>::type
twice(T) {
std::cout << "*" << std::endl;
}
// #2 数値型かつ符号なし整数型以外に対して有効となる関数テンプレート
template <typename T>
typename std::enable_if<
is_number<T>::value && !std::is_unsigned<T>::value
>::type
twice(T x) {
std::cout << (x * 2) << std::endl;
}
// #3 数値型かつ符号なし整数型に対して有効となる関数テンプレート
template <typename T>
typename std::enable_if<
is_number<T>::value && std::is_unsigned<T>::value
>::type
twice(T x) {
std::cout << (x * 2) << "u" << std::endl;
}
SFINAEによる強力な表現能力を使いこなすには、有効な関数オーバーロードをただ一つに定める制御 が必要となります。テンプレートパラメータに応じた条件分岐のために、ソースコード記述がこんなにも煩雑になってしまいました。2
Day6: 関数テンプレート×コンセプト
いよいよ真打ち登場、C++20コンセプトを使った実装がこちらになります。
#include <concepts>
#include <iostream>
// 数値型(整数or浮動小数点数)を判定するコンセプト
template <typename T>
concept Number = std::integral<T> || std::floating_point<T>;
// #1 プライマリ関数テンプレート(簡略構文)
void twice(auto) {
std::cout << "*" << std::endl;
}
// #2 数値型に対して優先される関数テンプレート(簡略構文)
void twice(Number auto x) {
std::cout << (x * 2) << std::endl;
}
// #3 符号なし整数型に対して最優先される関数テンプレート(簡略構文)
void twice(std::unsigned_integral auto x) {
std::cout << (x * 2) << "u" << std::endl;
}
旧来のSFINAE実装に比べると、デフォルトで選択されるプライマリテンプレート(#1)、数値型のためのテンプレート(#2)、符号なし整数型のためのテンプレート(#3)という 制約の階層構造(優先順位)が自然に表現されています。3
C++20コンセプトで導入された auto
による関数テンプレート簡略構文 も使って、見た目にもすっきりさせました。
おわりに
|・ω・*) C++20コンセプト、待ち遠しいですね。
...ま、必ずしも良いことばかりとは限りませんがね
(ΦωΦ) 深淵をのぞくとき、深淵もまたこちらを...
-
例示ソースコードはC++11準拠としました。C++17現在は
std::bool_constant
やメタ関数の_v
/_t
エイリアスを使うともう少し簡素に記述できます。また constexpr if文 を用いて関数テンプレート内部でコンパイル時分岐を行う実装も考えられます。 ↩ -
熟練LISPプログラマほど括弧
()
を意識しないと言われるのと同様、熟練C++テンプレートメタプログラマーであればtypename
,::value
,::type
等のノイズは目に入らないかもしれません。 ↩ -
本文中ソースコードのようにシンプルに記述できるのは、C++20ライブラリ提供の標準コンセプト
std::integral
,std::floating_point
,std::unsigned_integral
が適切に設計されているためです。 ↩