C++20 から導入される予定の機能に Concept というものがあります。 どういうものかというと型の継承関係に依存せずにある型が満たすべき性質を記述できるものです。

この辺りの機能を使うと型クラスのようなことができそうなので試してみました。

型クラスという言葉を初めて聞いた人向けに以下の記事をお勧めします。

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;
}
view raw concept.cpp hosted with ❤ by GitHub
https://wandbox.org/permlink/nsQsvAG3o637gcuI

  • OrdTraits: テンプレートの特殊化(traits パターンと呼ばれるらしい)
    • ここでは int に対して実装を用意しています
  • max の定義に使っている requires Ord<T> はなくてもコンパイルができます。
    • ただし、その場合に想定外の型に使われた場合のエラーがわかりづらい

つまり traits パターンを用いることでもともと型クラスに近いことができていたが、 Concept が導入されることでより安全で分かりやすいコードになったということのようです。

残念なポイントとして、 OrdTraitsOrd の間で名前が衝突してしまうので二つの名前を使い分ける必要があるところが挙げられます。 また、構文的にも非常に分かりずらいので(特に 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))
}
}
view raw implicit.scala hosted with ❤ by GitHub

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 -- リテラルの型を明示しないとこの場合は候補が絞れず怒られる
view raw type-class.hs hosted with ❤ by GitHub