[C++]SFINAEとC++20のコンセプトを比較してみる。

C++のテンプレートが大好物な私は以前から話題になっていたコンセプトとやらにもそこそこ興味を持っている。コンセプトとはざっくり言えば、今までSFINAEを使って実現していたテンプレートパラメータに対する制限やオーバーロードを、もっと分かりやすく実現するための方法である。そこ、SFINAEで十分だとか言っちゃだめ。
Visual Studio 2019が16.8 Preview1でIntelliSenseがコンセプトに対応したという話を聞き、折角なのでプレビュー版をインストールして遊んでみた。

コンセプトの基本は至るところで解説されているのでそれらを参照されたい。ここでは主に具体例をだらだらと書き並べてみる。何か思いついたらそのうち書き加えていくかも。

整数型、浮動小数点型、それ以外を呼び分ける。

template <class Type, std::enable_if_t<std::is_integral_v<Type>, std::nullptr_t> = nullptr>
void func_sfinae(Type x)
{
    std::cout << "x is integer " << x << std::endl;
}
template <class Type, std::enable_if_t<std::is_floating_point_v<Type>, std::nullptr_t> = nullptr>
void func_sfinae(Type x)
{
    std::cout << "x is float " << x << std::endl;
}
template <class Type, std::enable_if_t<(!std::is_integral_v<Type> && !std::is_floating_point_v<Type>), std::nullptr_t> = nullptr>
auto func_sfinae(Type x) -> decltype(std::cout << x, void())
{
    std::cout << "x is neither integral nor float " << x << std::endl;
}

template <class Type>
concept Integral = std::is_integral_v<Type>;//std::integralと全く同じ。
template <class Type>
concept Float = std::is_floating_point_v<Type>;//std::floating_pointと全く同じ。
template <class Type>//複数の条件を組み合わせることも出来る。
concept Other = !std::is_integral_v<Type> && !std::is_floating_point_v<Type> &&
requires (const Type& v)
{
    std::cout << v;
};

template <Integral Type>
void func_concept(Type x)
{
    std::cout << "x is integer " << x << std::endl;
}
template <Float Type>
void func_concept(Type x)
{
    std::cout << "x is float " << x << std::endl;
}
template <Other Type>
void func_concept(Type x)
{
    std::cout << "x is neither integral nor float " << x << std::endl;
}

int main()
{
    func_sfinae(3);
    func_sfinae(3.141592);
    func_sfinae("3.141592");

    func_concept(3);
    func_concept(3.141592);
    func_concept("3.141592");
    return 0;
}

Forward Iteratorを持つコンテナのみ許す。

Foward Iteratorを持つ、ということを、ここでは以下の条件で判定することにした。

  • begin()、end()という名前のメンバ関数を持つ。
  • begin()の戻り値は++演算子を適用できる。
  • begin()とend()の戻り値を比較可能である。またその戻り値はboolへ変換することが出来る。

上でもちょっとだけ使っていたが、クラスが何らかのメンバ関数を持つことを要求する場合、SFINAEでは戻り値を定義するdecltypeを駆使すれば可能である。が、初心者にはstd::enable_if以上に分かりにくい気がする。decltypeの中のコンマが実は演算子で末尾の値が戻り値になるとか、分かる人はどのくらいいるんだろう。コンマが演算子として扱われること、実はオーバーロードできることとかはC++の黒魔術っぷりに拍車をかけている。

template <class Type>
auto func2_sfinae(Type& x) -> decltype(++x.begin(), x.end(), bool(x.begin() != x.end()), void())
{
    for (auto& v : x)
    {
        std::cout << v << " ";
    }
    std::cout << std::endl;
}

template <class Type>
concept STLContainer = requires(Type & t)
{
    t.begin()++;
    { t.begin() != t.end() } -> std::convertible_to<bool>;
};

template <STLContainer Type>
void func2_concept(Type& x)
{
    for (auto& v : x)
    {
        std::cout << v << " ";
    }
    std::cout << std::endl;
}

int main()
{
    std::set<int> v{ 1, 3, 5, 7 };
    func2_sfinae(v);
    func2_concept(v);
    return 0;
}

可変長引数テンプレートに条件を設ける。

一つコンセプトのありがたいところは、可変長引数テンプレートで型の制限を設けやすくなったことだ。C++で可変個引数関数を実現したい場合、一般に可変長引数テンプレートを使うのだが、例えばこの可変個の引数が全てint型であってほしい場合、SFINAEでは記述が長たらしくなる。今回はstd::is_sameのみで表現できるためまあそこまで汚くはないが、複雑な条件を要求するときは酷いものである。C++17の畳み込み式がなかった頃はさらに面倒だった。

template <class ...Types,
          std::enable_if_t<(std::is_same_v<Types, int> && ...), std::nullptr_t> = nullptr>//長い!汚い!
void func3_sfinae(Types ...x)
{
    (std::cout << ... << x) << std::endl;
}

//requires節でも多少は楽になる。
template <class ...Types>
requires (std::same_as<Types, int> && ...)
void func3_concept(Types ...x)
{
    (std::cout << ... << x) << std::endl;
}

template <class Type>
concept Int = std::same_as<Type, int>;

//事前に定義されたコンセプトを使えばもっとすっきりする。
template <Int ...Types>
void func3_concept2(Types ...x)
{
    (std::cout << ... << x) << std::endl;
}

int main()
{
    func3_sfinae(1, 2, 3, 4);
    func3_concept(1, 2, 3, 4);
    func3_concept2(1, 2, 3, 4);
    return 0;
}

実のところ、バージョンが新しくなったからと言って出来ることが増えるわけではない。いや、もちろん多数の機能は追加されるのだが、それは“昔よりも大幅にパフォーマンスが改善する”とか“今まで不可能だった設計が実現する”というような革新的なものではない。テンプレートやC++11のムーブセマンティクスは設計方法そのものを根幹から覆すような画期的なものだったと思うが、それ以外はさほど重大ではない。今まで苦労して実現していたことが、幾分楽になるだけのことである。知識さえあればそれなりに手間ひまかけて実現させられたことを、知識さえあればちょっと簡単に実現できるようにするだけである。いやプログラミング言語ってそんなものなのかもしれないけど。
そういう意味で、コンセプトは面白い機能だが、今までSFINAEや特殊化で実現していたところを置き換えるだけで、新しい動作が可能になるわけでなさそうである。……正直に言うとちょっとがっかりしている。
まあでも、格段にコーディングしやすくなることは分かった。何年か経って世間的にC++20が浸透したらぜひ導入したい機能には違いない。