LoginSignup
20
15

More than 5 years have passed since last update.

C++03/C++11(14)/C++17でのmember detector

Last updated at Posted at 2018-12-17

はじめに

この記事は、C++ Advent Calendar 2018 の18日目です。

前日の記事は @_EnumHack さん
明日の記事は @Kogia_sima さんです。

本記事のテーマはC++erの大好きなmember detectorです。

Member detectorとは?

さてMember detectorとは、なんでしょうか?More C++ Idiomsのこちらの記事から引用してみます。

Compile-time reflection capabilities are the cornerstone of C++ template meta-programming. Type traits libraries such as Boost.TypeTraits and TR1 header provide powerful ways of extracting information about types and their relationships. Detecting the presence of a data member in a class is also an example of compile-time reflection.

ふむ。要するに、コンパイル時にあるクラスが特定のメンバを持っているかどうかを確認するためのイディオムということですね。

そんなもの何に使うの?と思うかもしれませんが、テンプレートを用いたGeneric programmingにおいて、テンプレートパラメータに制約を課せるということは、非常に有用です。例えば、

template <typename T> inline
void print_ok_or_ng(const T& o) {
    if (o.size() == 0) {
        std::cout << "NG" << std::endl;
        return;
    }
    std::cout << "OK" << std::endl;
}

みたいな処理があったとしましょう。当然Tにはどんな型でも入ってしまうため、

do_something(1); // だめ! int は size() をもってない!

こんなコードを書いてしまうかもしれません。
まぁ、このくらいであれば、コンパイルエラーを見れば、ああなーんだdo_something内でo.size()してる箇所が悪いんだな〜、とすぐにわかると思います。
しかし、実際にコーディングしていくと、関数呼び出しのネストはどんどん深くなっていき、

template <typename T> inline
bool is_empty(const T& o) {
    return o.size() == 0;    // ここでエラーになる
}

template <typename T> inline
bool check_length(const T& o) {
    return !is_empty(o);
}

template <typename T> inline
bool check(const T& o) {
    return check_length(o);
}

template <typename T> inline
void print_ok_or_ng(const T& o) {    // こいつに変なのをいれたのが原因だ!
    if (check(o)) {
        std::cout << "OK" << std::endl;
        return;
    }
    std::cout << "NG" << std::endl;
}

という感じで、問題のある箇所を探すのがどんどん大変になるため、genericな関数を書くのであれば、なるべく入口側でエラーになってくれるのが好ましいのです。

そこで出てくるのがmember detectorです。このmember detectorというのは、ある特定のクラスが、メンバ関数をもっているかだったり、ネストされた型をもっているかを判定してくれるものでしたね。
もし、上記のようなシチュエーションでメタ関数has_sizeのようなものがあった場合、

template <typename T> inline
typename std::enable_if<
    has_size<T>::value,
    void
>::type
print_ok_or_ng(const T& o) {    // Tがsize()を持ってない時点でオーバーロードの候補から外れる!
    if (check(o)) {
        std::cout << "OK" << std::endl;
        return;
    }
    std::cout << "NG" << std::endl;
}

としておくことで、そもそも引数としてsize()を持っていないものをいれられなくなります。
では、このhas_sizeってどうやって作ればいいの?というのが本稿のテーマです。

書いてみる

さて、今回はあるクラスTがメンバ関数size()を持つかどうかをチェックするhas_size、またネスト型size_typeを持つかどうかをチェックするhas_size_typeを作ります。

C++03

昔懐かしいC++03です。昔懐かしいと言っても、VisualStudio2008が現役な組織も普通にあると思いますので、亡き者とするにはまだまだ早い感じがしますね。
ただ、C++03では、どうしてもメンバ関数のdetectionコードの書き方が思いつきませんでした…(てか書けるのかな?)。

#include <iostream>
#include <vector>

template <typename T>
struct has_size_type {
private:
    typedef short ok;
    typedef long ng;

    template <typename U>
    static ok test(typename U::size_type);

    template <typename U>
    static ng test(...);

public:
    static const bool value = sizeof(test<T>(0)) == sizeof(ok);
};

int main() {
    std::cout 
        << std::boolalpha
        << has_size_type<std::vector<int> >::value << std::endl  // true
        << has_size_type<int>::value << std::endl;               // false

    return 0;
}

typedefやテンプレートパラメータの綴じ括弧を連続させないなど、非常に味わい深いコードですね。ポイントは以下の2点です。

  • U::size_typeがなかった場合でもSFINAEによりエラーにはならず、次のオーバーロード候補が探され、引数が...は最も優先順位が低い
  • sizeofは実際には関数を呼び出さず、結果のサイズのみを返してくれますので、OK時とNG時で別の大きさになるようなデータ型を指定している

C++11/C++14

さて次はC++11/C++14です。decltype爆誕のおかげで、意味不明のsizeofとかすることなく、かなりスッキリと書けるようになりました。

#include <iostream>
#include <type_traits>
#include <utility>
#include <vector>

template <typename T>
struct has_size_type {
private:
    template <typename U>
    static auto test(int) -> decltype(std::declval<typename U::size_type>(), std::true_type());

    template <typename U>
    static auto test(...) -> decltype(std::false_type());

public:
    using type = decltype(test<T>(0));
    static constexpr bool value = type::value;
};

template <typename T>
struct has_size {
private:
    template <typename U>
    static auto test(int) -> decltype(std::declval<U>().size(), std::true_type());

    template <typename U>
    static auto test(...) -> decltype(std::false_type());

public:
    using type = decltype(test<T>(0));
    static constexpr bool value = type::value;
};


int main() {
    std::cout
        << std::boolalpha
        << has_size_type<std::vector<int>>::value << std::endl   // true
        << has_size_type<int>::value << std::endl                // false
        << has_size<std::vector<int>>::value << std::endl        // true
        << has_size<int>::value << std::endl;                    // false


    return 0;
}

オーバーロードの優先順位を使っている部分はC++03のときと同じですが、usingstd::true_typeなどが使えるのはとてもありがたいですね。ポイントは以下の2点です。

  • decltypeは式を受け取り、コンマオペレーターで繋がれたものは式として扱われる。前から評価されていき、一番末尾にある型が式の型となる。
  • declvalを使うことで参照を作れるので、コンストラクタが呼び出せなくてもOK

デフォルトテンプレートパラメータを使ったか書き方や、継承を用いるものなどもメジャーなので、もし興味があれば調べてみましょう。

C++17

最後にC++17です。void_tという、いつだってvoidを返してくれるメタ関数が誕生し、世の中のmember detectionは様変わりしました。

#include <iostream>
#include <type_traits>
#include <vector>
#include <experimental/type_traits>

template <typename T, typename =void>
struct has_size_type : std::false_type {};

template <typename T>
struct has_size_type<T, std::void_t<typename T::size_type>> : std::true_type {};

template <typename T>
using size_func = decltype(std::declval<T>().size());

template <typename T>
using has_size = std::experimental::is_detected<size_func, T>;

int main() {
    std::cout
        << std::boolalpha
        << has_size_type<std::vector<int>>::value << std::endl   // true
        << has_size_type<int>::value << std::endl                // false
        << has_size<std::vector<int>>::value << std::endl        // true
        << has_size<int>::value << std::endl;                    // false

    return 0;
}

どん!どうですか、このわかりやすさは。
このなんてことないメタ関数void_tのおかげで、正直これ以外で見たことも聞いたこともない(...)などという謎表記から開放され、おなじみのテンプレートの特殊化でmember detectionができるようになりました。ポイントは以下の2点です。

  • T::size_typeがあった場合、特殊化されているほうが優先される
  • is_detectedなるものがexperimentalではあるもののすでに用意されてる

ありがとうC++。もう黒魔術とは言わせない。

おわりに

さて、SFINAEといえばmember detectionといわれるほどメジャーな技法ですが、C++17の領域まで来ると、もはや技法とも言えないレベルの簡素さですね。

しかしexperimental/type_traitsは、所詮experimentalですし、なぜかVisual Studioには実装されていなかったりもするので、まだまだ暫くはC++11/14の書き方で戦うのかなぁという印象です。ただ、is_detected自体は難しい実装でもないので自前で書いてる人も多そうです。

そーす

おまけ

ナウなキッズ「trait boundで、簡単に書けるよ。そうRustならね。」

20
15
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
15