C++20 から導入される予定の機能に Concept というものがあります。 どういうものかというと型の継承関係に依存せずにある型が満たすべき性質を記述できるものです。
この辺りの機能を使うと型クラスのようなことができそうなので試してみました。
型クラスという言葉を初めて聞いた人向けに以下の記事をお勧めします。
- Cats: https://typelevel.org/cats/typeclasses.html
- ドワンゴ: https://dwango.github.io/scala_text/introduction-to-typeclass.html
Concept の詳細な説明は下記のサイトに譲るとして、ここでは簡単な紹介といきなり具体的なコードに移ろうと思います。 (短いコード片などは以下のサイトから引用しています)
また、コンパイルできる環境を用意するのは大変なので Wandbox を使用します。
template<typename T>
concept bool Addable = requires (T x) { x + x; };
template<typename T> requires Addable<T>
T add(T a, T b) { return a + b; }
このようにするとある型 T
の値に +
という T
に閉じた二項演算子を要求する Addable
というコンセプト、
それから、Addable
を用いて型に制約をつけている関数 add
が定義できます。
int main()
{
std::cout << add("", "") << std::endl;
}
このような記述をした上でコンパイルすると以下のようなエラーが出力されます。 試してみたい方はこちら
prog.cc: In function 'int main()':
prog.cc:12:28: error: cannot call function 'T add(T, T) [with T = const char*]'
12 | std::cout << add("", "") << std::endl;
| ^
prog.cc:8:3: note: constraints not satisfied
8 | T add(T a, T b) { return a + b; }
| ^~~
prog.cc:5:14: note: within 'template<class T> concept const bool Addable<T> [with T = const char*]'
5 | concept bool Addable = requires (T x) { x + x; };
| ^~~~~~~
prog.cc:5:14: note: with 'const char* x'
prog.cc:5:14: note: the required expression '(x + x)' would be ill-formed
さて、なんとなく Concept の気持ちがわかってきたところで本題に入ります。
早速ですが、普段 C++ 書かないなりにひねり出したのが以下のコードです。
#include <iostream> | |
#include <cstdlib> | |
template<typename T> | |
struct OrdTraits; | |
// int 向けにテンプレートの特殊化 | |
template<> struct OrdTraits<int> { | |
static bool less(const int &x, const int &y) { | |
return x < y; | |
} | |
}; | |
// concept を定義 | |
template<typename T> | |
concept bool Ord = requires (T x) { | |
{ OrdTraits<T>::less(x, x) } -> bool | |
}; | |
template <class T> requires Ord<T> // requires で T が Ord<T> を満たすことを要求 | |
T max(const T &x, const T &y) | |
{ | |
return OrdTraits<T>::less(x, y) ? y : x; | |
} | |
int main() | |
{ | |
std::cout << max(1, 10) << std::endl; | |
} |
- OrdTraits: テンプレートの特殊化(traits パターンと呼ばれるらしい)
- ここでは
int
に対して実装を用意しています
- ここでは
- max の定義に使っている
requires Ord<T>
はなくてもコンパイルができます。- ただし、その場合に想定外の型に使われた場合のエラーがわかりづらい
つまり traits パターンを用いることでもともと型クラスに近いことができていたが、 Concept が導入されることでより安全で分かりやすいコードになったということのようです。
残念なポイントとして、 OrdTraits
と Ord
の間で名前が衝突してしまうので二つの名前を使い分ける必要があるところが挙げられます。
また、構文的にも非常に分かりずらいので(特に traits 周り)実用したいかと言われるとなんとも言えないところですね。
おまけ
Scala
Scala で上記のサンプルと同じことをかくとこんな感じ。(Wandbox で動かせる)
trait Ord[A] { | |
def less(x: A, y: A): Boolean | |
} | |
object OrdInstances { | |
// Ord[A] に一つしか抽象メソッドがないため SAM Conversion が行われる | |
// http://eed3si9n.com/ja/scala-2.12.0 | |
implicit val intOrd: Ord[Int] = (x: Int, y: Int) => x < y | |
} | |
object Wandbox { | |
// implicit な値をこのスコープに導入 | |
import OrdInstances._ | |
// カリー化されている最後の引数リストで Ord[A] の暗黙の値を受け取っている | |
def max[A](x: A, y: A)(implicit ord: Ord[A]): A | |
= if (ord.less(x, y)) y else x | |
def main(args: Array[String]): Unit = { | |
// max(1, 10)(intOrd) でも同じ | |
println(max(1, 10)) | |
} | |
} |
Haskell
Haskell で上記のサンプルと同じことをかくとこんな感じ。(Wandbox で動かせる)
import Prelude hiding (Ord, max) -- GHC の標準に Ord, max が存在するので明示的に使わない設定する | |
class Ord a where | |
less :: a -> a -> Bool | |
instance Ord Int where | |
less x y = x < y | |
max :: Ord a => a -> a -> a | |
max x y = if (less x y) then y else x | |
main :: IO () | |
main = putStrLn $ show $ max (1 :: Int) 10 -- リテラルの型を明示しないとこの場合は候補が絞れず怒られる |