2013年12月7日土曜日

[Visual C++] __if_exists を使ってみた

C++ Advent Calendar 2013
こちらは C++ Advent Calendar 2013 7日目の記事になります。
6日目は hgodai さんで「scoped_shared_ptrとstatic_allocatorを使ったstatic_map」でした。
8日目は tt_clown さんで「Boost 逆引きリファレンスのサンプルコードをテスト化してみる - Life like a clown」です。

当初、頭の中で考えていた妄想コードを書き起こしてうまくいったら記事にしようと思っていたのですが、
自分の脳内コンパイラーがヘボかったため、ダメでした。
というわけで、別件で書きためていた __if_exists の記事を使うことにしました。

「VC++ なる C++ に若干似たコンパイラ」のお話で大変恐縮です。<(・∀・)


はじめに
Visual Studio には __if_exists という独自機能があります。どういったものかというと、
__if_exists ( identifier ) { 
statements
};
identifier に渡した識別子が存在するならば、statements が実行される代物です。
否定形には __if_not_exists があります。

最近 __if_exists を使う機会があったので少しまとめてみました。
※ 前半は個人的に試してみたこと、後半は C++11/C++14 での検証になってます。
※ サンプルコードは include を省略してあります。

template + __if_exists で関数の呼び分け
まずは、MSDN の使用例にも書かれている関数の呼び分けです。

template<typename T>
class X : public T {
public:
    void Dump() {
        __if_exists(T::Dump) {
            T::Dump();
        }
        __if_not_exists(T::Dump) {
            ::std::cout << "T::Dump does not exist" << ::std::endl;
        }
    }
};

class A {
public:
    void Dump() {
        ::std::cout << "A::Dump()" << ::std::endl;
    }
};

class B {};

int main() {
    X<A> x1;
    X<B> x2;

    x1.Dump();
    x2.Dump();

    return 0;
}

出力
A::Dump()
T::Dump does not exist

このように関数や変数が存在したらそれを使い、存在しなければ別の方法を示すことができるのが、
__if_exists/__if_not_exists です。

多重定義の抑制
自作テスティングフレームワーク(iutest)の作成中、以下のようなコードを書けるようにしたいと思いました。
// hoge.h
IUTEST_PACKAGE(hoge_test) {
  // hogehoge
}

// hoge.cpp
#include "hoge.h"

IUTEST_PACKAGE(hoge_test) {
  // hogehoge
}

IUTEST_PACKAGE の実装はこのようになっています。
#define IUTEST_PACKAGE(name)     \
    namespace name {                                    \
    class iuTest_TestCasePackage;                       \
    static ::std::string IUTEST_ATTRIBUTE_UNUSED_       \
    iuTest_GetTestCasePackageName(const iuTest_TestCasePackage*) {  \
        return iuTest_GetTestCaseParentPackageName(static_cast<iuTest_TestCaseParentPackage*>(NULL)) + #name ".";   \
    }                                                   \
    class iuTest_TestCaseParentPackage;                 \
    static ::std::string IUTEST_ATTRIBUTE_UNUSED_       \
    iuTest_GetTestCaseParentPackageName(const iuTest_TestCaseParentPackage*) {              \
        return iuTest_GetTestCasePackageName(static_cast<iuTest_TestCasePackage*>(NULL));   \
    }                                                   \
    }                                                   \
    namespace name

IUTEST_PACKAGE マクロは名前空間の定義と特定の関数を定義します。
関数を定義するため、複数の関数定義をしてしまいコンパイルエラーとなっていました。
当時は諦めたのですが・・・ __if_not_exists 使えば、実現できることに気づいたので実装してみました。

※ ちなみに、関数でなく変数であれば多重定義できるようです。
> 同じスコープ内に重複した名前を宣言する - にっき(pseudo)

2回目以降の IUTEST_PACKAGE のときに iuTest_GetTestCasePackageName 関数と iuTest_GetTestCaseParentPackageName 関数を定義しなければよいので、__if_not_exists を使って以下のように実装しました。

#define IUTEST_PACKAGE(name)         \
    namespace name {                                        \
    class iuTest_TestCasePackage;                           \
    __if_not_exists(name::iuTest_GetTestCasePackageName) {  \
        static ::std::string IUTEST_ATTRIBUTE_UNUSED_       \
        iuTest_GetTestCasePackageName(const iuTest_TestCasePackage*) {  \
            return iuTest_GetTestCaseParentPackageName(static_cast<iuTest_TestCaseParentPackage*>(NULL)) + #name ".";   \
        }                                                   \
    }                                                       \
    class iuTest_TestCaseParentPackage;                     \
    __if_not_exists(name::iuTest_GetTestCaseParentPackageName) { \
        static ::std::string IUTEST_ATTRIBUTE_UNUSED_       \
        iuTest_GetTestCaseParentPackageName(const iuTest_TestCaseParentPackage*) {              \
            return iuTest_GetTestCasePackageName(static_cast<iuTest_TestCasePackage*>(NULL));   \
        }                                                   \
    }                                                       \
    }                                                       \
    namespace name

これで、IUTEST_PACKAGE マクロが何回でも書け、統一的なソースコードにすることができるようになりました。
(※ Visual Studio でのみの機能になるのであまりオススメはしない)
※ 宣伝
iutest は Google Test ライクでヘッダーオンリーなテスティングフレームワークです。
gtest にはない機能、C++11 対応など積極的に取り込んでいますので是非一度見てもらえると嬉しいです。ご意見などいただけるとなお嬉しいです。

隠蔽の検出
「Effective C++ 33項:継承した名前を隠蔽しないようにしよう」にあるような、
名前の隠蔽を検出することができるのでは?と思って書いてみました。

#define CHECK_HIDING(base, name) \
  __if_exists( base::name ){ static_assert(false, #name " is hiding"); }
#define CHECK_HIDING2(name) CHECK_HIDING( __super, name )

class Base {
protected:
    int m_hoge;
public:
    int GetHoge(void) const { return m_hoge; }
};

class Derived : public Base {
public:
    int m_hoge;
    int m_fuga;

    CHECK_HIDING(Base, m_fuga);
    CHECK_HIDING(Base, m_hoge);

    CHECK_HIDING2(m_fuga);
    CHECK_HIDING2(m_hoge);

    Derived() {
    }
};

結論としては、失敗。
Visual Studio には __super という拡張機能があります。
これは自身のスーパークラス(基底クラス)を指すキーワードです。

__super を使うことで基底クラスへのスコープが統一的に書け、
マクロに明示的に基底クラス名を指定しなくて済むと思ったのですが…

実際には、CHECK_HIDING2 で以下の様なエラーが吐かれます。
error C2760: 構文エラー : '識別子' でなければなりませんが、'__super' が使われています。

残念でした。
__super を使わないにしろ基底クラスをユーザーが明記しないといけないので、あまり良いマクロとはいえないですね。

C++11 との組み合わせ
ここから後半戦。__if_exists を様々な場所で使ってみました。

型推論
auto 変数と組み合わせて問題がないか検証しました。
class A {
public:
    static float get() { return 1.2f;  }
};
class B {};

int main() {
    auto a = __if_exists(A::get) {
        A::get();
    }
    __if_not_exists(A::get) {
        "not found";
    }

    auto b = __if_exists(B::get) {
        B::get();
    }
    __if_not_exists(B::get) {
        "not found";
    }

    auto c = [](){ 
        __if_exists(A::get) { return A::get(); }
        __if_not_exists(A::get) { return "not found"; }
    }();

    ::std::cout << a << ::std::endl;
    ::std::cout << b << ::std::endl;
    ::std::cout << c << ::std::endl;
    return 0;
}

出力
B::Dump
1.2
not found

問題なく使えました。
result(C++ Compiler Farm)

lambda
int main()
{
    int x=0, y=1;
    auto fa = [__if_exists(x) { x } __if_not_exists(x) { y } ]()
    { 
        __if_exists(x) {
            ::std::cout << x << ::std::endl;
        }
        __if_not_exists(x) {
            ::std::cout << y << ::std::endl;
        }
    };
    auto fb =[__if_exists(z) { z } __if_not_exists(z) { y }]()
    {
        __if_exists(z) {
            ::std::cout << z << ::std::endl;
        }
        __if_not_exists(z) {
            ::std::cout << y << ::std::endl;
        }
    };

    fa();
    fb();
}

出力
0
1
キャプチャ[]の中でも問題なく使えました。
result(C++ Compiler Farm)

decltype
■ 基本
decltype の中で使えます。
typedef decltype( 
    __if_exists( T::Dump ) { T::Dump() }
    __if_not_exists( T::Dump ) { 0 }
    ) type;

T::Dump があれば T::Dump 関数の戻り値型、なければ decltype( 0 ) になります。

■ 継承
基底クラスの選択に decltype と __if_exists を使います。
class A {
public:
    static void Dump() { ::std::cout << "A::Dump" << ::std::endl; }
};
class B {
public:
    static void Dump() { ::std::cout << "B::Dump" << ::std::endl; }
};

#define TEST1(name, base) class name : \
    public __if_exists(base) { base } __if_not_exists(base) { A } {};
#define TEST2(name, base) class name : \
    public decltype( __if_exists(base) { base() } __if_not_exists(base) { A() }) {};

TEST1(X1, B);
TEST1(Y1, C);
TEST2(X2, B);
TEST2(Y2, C);

int main()
{
    X1::Dump();
    Y1::Dump();
    X2::Dump();
    Y2::Dump();

    return 0;
}

出力
B::Dump
A::Dump
B::Dump
A::Dump

※ Visual Studio 2013 で確認。2012 ではビルドできず。
まぁ、decltype 使わなくてもできるのですが、問題なくビルドできたので。

■ 戻り値を後ろに置く関数構文
class Test1 {
public:
    static float Dump() {
        ::std::cout << "Test1::Dump" << ::std::endl;
        return 0.1f;
    }
};
class Test2 {
public:
    static int  Print() {
        ::std::cout << "Test2::Print" << ::std::endl;
        return 1;
    }
};


template<typename T>
auto F(void) ->
    decltype( __if_exists(T::Dump) { T::Dump() }
        __if_exists(T::Print) { T::Print() } )
{
    __if_exists(T::Dump ) { return T::Dump();  }
    __if_exists(T::Print) { return T::Print(); }
}

int main(int argc, const char* argv[])
{
    auto var1 = F<Test1>();
    auto var2 = F<Test2>();

    ::std::cout << var1 << ::std::endl;
    ::std::cout << var2 << ::std::endl;

    return 0;
}

出力
Test1::Dump
Test2::Print
0.1
1

※ Visual Studio 2013 で確認。2012 ではビルドできず。
こちらも問題なく動作。

= delete
delete した関数はどう判断されるのか試してみました。

class A {
public:
    ~A() = delete;
    void F() = delete;
};

int main()
{
    __if_exists(A::~A) {
        ::std::cout << "A::~A found" << ::std::endl;
    }
    __if_not_exists(A::~A) {
        ::std::cout << "A::~A not found" << ::std::endl;
    }

    __if_exists(A::F) {
        ::std::cout << "A::F found" << ::std::endl;
    }
    __if_not_exists(A::F) {
        ::std::cout << "A::F not found" << ::std::endl;
    }

    __if_exists(A::G) {
        ::std::cout << "A::G found" << ::std::endl;
    }
    __if_not_exists(A::G) {
        ::std::cout << "A::G not found" << ::std::endl;
    }

    return 0;
}

出力
A::~A found
A::F found
A::G not found

どうやら delete したものも、__if_exists が真になるようです。
当然 delete された関数は呼ぶことができないので、
このようなコードはエラーになります。
class A {
public:
    void Dump() = delete;
};

int main() {
    A a;
    __if_exists(A::Dump) {
        a.Dump();
    }
    return 0;
}

main.cpp(10): error C2280: 'void A::Dump(void)' : 削除された関数を参照しようとしています
= delete された関数は存在しないと判別されるのが自然な感じなのですが…
まだ対応できていない、ということなのでしょうかね?

using
C++11 では別名テンプレートが使えるようになったので、それでも検証。
template<typename T, typename U>
class A {
    T t;
    U u;
};

template<typename U>
using B = A<int, U>;
using C = A<int, int>;
typedef A<int, int> D;

#define CHECK(name)     \
    __if_exists(name) { \
        ::std::cout << #name " found" << ::std::endl    \
    }                   \
    __if_not_exists(name) {\
        ::std::cout << #name " not found" << ::std::endl    \
    }


int main()
{
    CHECK(A);
    CHECK(B);
    CHECK(C);
    CHECK(D);
    CHECK(A::t);
    CHECK(B::t);
    CHECK(B::u);
    CHECK(C::t);
    CHECK(D::t);

    __if_exists(B<float>) {
        ::std::cout << "B<float> found" << ::std::endl;
    }
    __if_not_exists(B<float>) {
        ::std::cout << "B<float> not found" << ::std::endl;
    }

    __if_exists(A<int, int>::t) {
        ::std::cout << "A<int, int>::t found" << ::std::endl;
    }
    __if_not_exists(A<int, int>::t) {
        ::std::cout << "A<int, int>::t not found" << ::std::endl;
    }

    return 0;
}

出力
A found
B found
C found
D found
A::t not found
B::t not found
B::u not found
C::t found
D::t found
B<float> not found
A<int, int>::t not found

非template な型の名前でないとダメなようです。
typedef や 型が決定的な using の場合は、正しく判別できるもよう。

__if_exists(A<int, int>::t) って書けたら嬉しいんだけどなぁ

Visual C++ Compiler November 2013 CTP
Visual C++ Compiler November 2013 CTP で対応した機能も調べてみました。

constexpr
みんな大好き constexpr
static const int a=1;

constexpr int fa(void)
{
    __if_exists(a) {
        return a;
    }
    __if_not_exists(a) {
        return 0;
    }
}
constexpr int fb(void)
{
    __if_exists(b) {
        return b;
    }
    __if_not_exists(b) {
        return 0;
    }
}

int main() {
    ::std::cout << fa() << ::std::endl;
    ::std::cout << fb() << ::std::endl;
    return 0;
}

出力
1
0

問題なく使えました。

C++14 auto function return type deduction
関数の戻り値型の型推論です。
static const char a[]="test";
static const double c=0.1;

auto f(void)
{
    __if_exists(a) {
        return a;
    }
    __if_not_exists(a) {
        return 0;
    }
}
auto g(void)
{
    __if_exists(b) {
        return b;
    }
    __if_not_exists(b) {
        __if_exists(c) {
            if( c > 0 )
                return c;
            else
                return 0.2;
        }
        __if_not_exists(c) {
            return 0;
        }
    }
}

int main() {
    ::std::cout << f() << ::std::endl;
    ::std::cout << g() << ::std::endl;
    return 0;
}

出力
test
0.1

これも問題なし。

C++14 generic lambdas
汎用ラムダキャプチャで試そうと思ったが、まだ非対応でした。
int main()
{
    int x=0, y=1;
    // まだできない
    [a= __if_exist(x) { x } __if_not_exist(x) { y }]()
        { ::std::cout << a << ::std::endl; }();
    return 0;
}


まとめ
長々と書き連ねましたが、まとまりのない内容でホント申し訳ありませんm(__)m
とりあえず、C++11 と __if_exists の組み合わせに関しては
= delete したときだけ気をつければだいたい使えそうな感じ
ってのがわかりました。

若干、気になる部分もありますが __if_exists 自体は非常に強力な機能です。
ただし、コンパイラに依存するのでその辺だけ気をつけて使うようにしたいです。
以上。

C++ Advent Calendar 2013 8日目は tt_clown さんです。
よろしくお願いします。

0 件のコメント:

コメントを投稿