2015年12月9日水曜日

[Catch] REQUIRE の式が展開される仕組み

こちらは C++ Advent Calendar 2015 9日目の記事になります。
前の日は plasma_effector さんで「plasma.ADTの紹介 」でした。


ブログズミ: C++ Testing Framework の Catch を使ってみた
以前、Catch を紹介したときに REQUIRE マクロに渡した式が展開されることを書きました。

その仕組について、簡単ではありますが説明しておこうと思います。
(大分昔に書き上げていたものの、アップするタイミングを失っていたのですが、こうしてアドベントカレンダーとして公開することができました。)

#define CATCH_CONFIG_MAIN // main の定義を catch に任せる
#include <catch.hpp>

int f()
{
    return 1;
}

TEST_CASE("Test", "[sample]")
{
    CHECK( f() == 2 );
    REQUIRE( f() <= 0 );
}

-------------------------------------------------------------------------------
Test
-------------------------------------------------------------------------------
main.cpp(9)
...............................................................................

main.cpp(11): FAILED:
  CHECK( f() == 2 )
with expansion:
  1 == 2

main.cpp(12): FAILED:
  REQUIRE( f() <= 0 )
with expansion:
  1 <= 0

===============================================================================
1 test case - failed (2 assertions - both failed)

こちらが「ブログズミ: C++ Testing Framework の Catch を使ってみた」で書いたコードと実行結果です。

REQUIRE および CHECK に渡した式の中の f() が返した値が出力されています。
この仕組を説明します。


マクロを追う
REQUIRE や CHECK マクロがどのような実装になっているのか追っていきます。

まず、REQUIRE や CHECK はこのように書かれています。
#define REQUIRE( expr ) INTERNAL_CATCH_TEST( expr, Catch::ResultDisposition::Normal, "REQUIRE" )
#define CHECK( expr ) INTERNAL_CATCH_TEST( expr, Catch::ResultDisposition::ContinueOnFailure, "CHECK" )

どちらも INTERNAL_CATCH_TEST を使っています。
INTERNAL_CATCH_TEST は以下のようになっています。
#define INTERNAL_CATCH_TEST( expr, resultDisposition, macroName ) \
    do { \
        Catch::ResultBuilder __catchResult( macroName, CATCH_INTERNAL_LINEINFO, #expr, resultDisposition ); \
        try { \
            ( __catchResult->*expr ).endExpression(); \
        } \
        catch( ... ) { \
            __catchResult.useActiveException( Catch::ResultDisposition::Normal ); \
        } \
        INTERNAL_CATCH_REACT( __catchResult ) \
    } while( Catch::isTrue( false && (expr) ) ) // expr here is never evaluated at runtime but it forces the compiler to give it a look
(include/internal/catch_capture.hpp)

REQUIRE と CHECK の違いは resultDisposition と macroName の部分です。
macroName はただの名前なので特に気にしなくて OK です。 resultDisposition では続行可能か不可能かを分けています。

さて、ここで重要なのが
( __catchResult->*expr ).endExpression();
のところです。
->* は間接メンバポインタ演算子という演算子で、オーバーロード可能な演算子の1つです。
これが f() の戻り値が表示される仕組みの肝になります。

実際に最初のコードを例に説明をします。

式の分解
最初に示したコードの REQUIRE 文を展開します。

do {
    Catch::ResultBuilder __catchResult( "REQUIRE", CATCH_INTERNAL_LINEINFO, "f() <= 0", Catch::ResultDisposition::Normal );
    try {
        ( __catchResult->*f() <= 0 ).endExpression();
    }
    catch( ... ) {
        __catchResult.useActiveException( Catch::ResultDisposition::Normal );
    }
    INTERNAL_CATCH_REACT( __catchResult )
} while( Catch::isTrue( false && (f() <= 0) ) )

さて、改めて
( __catchResult->*expr ).endExpression();
に注目しましょう。
これは上記の展開されたコードでは、
( __catchResult->*f() <= 0 ).endExpression();
のようになっています。
ここで、
__catchResult->*f() <= 0
はどのような順番で処理されると思いますか?



正解は、
  1. f()
  2. __catchResult->*f()
  3. __catchResult->*f() <= 0
です。
これは演算子の優先順位により、<= よりも ->* が先に評価されるためです。
C++ Operator Precedence - cppreference.com

簡単に挙動確認コードを書きました。気になる方はこちらも参照してください。
[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

というわけで、「f() <= 0」 から 「f()」と「<= 0」とに分解できました。
ResultBuilder(__catchResult) には f() の戻り値が入ってくるので、f() の結果が Catch できるのです^^

分解できたのであとはイイ感じするだけです。ついでなので説明します。

残った式の処理
Catch::ResultBuilder::operator ->* を詳しく見てみましょう。
class ResultBuilder {
public:
    ResultBuilder(  char const* macroName,
                    SourceLineInfo const& lineInfo,
                    char const* capturedExpression,
                    ResultDisposition::Flags resultDisposition );

    template<typename T>
    ExpressionLhs<T const&> operator->* ( T const& operand );
    ExpressionLhs<bool> operator->* ( bool value );

template<typename T>
inline ExpressionLhs<T const&> ResultBuilder::operator->* ( T const& operand ) {
    return ExpressionLhs<T const&>( *this, operand );
}

inline ExpressionLhs<bool> ResultBuilder::operator->* ( bool value ) {
    return ExpressionLhs<bool>( *this, value );
}
(include/internal/catch_result_builder.h)

ResultBuilder::operator->* はなんらかの値を受け取り、ExpressionLhs を返します。
(bool で特殊化されていますがここでは無視します)

ExpressionLhs はこのように定義されています。
template<typename T>
class ExpressionLhs {
    ExpressionLhs& operator = ( ExpressionLhs const& );
#  ifdef CATCH_CPP11_OR_GREATER
    ExpressionLhs& operator = ( ExpressionLhs && ) = delete;
#  endif

public:
    ExpressionLhs( ResultBuilder& rb, T lhs ) : m_rb( rb ), m_lhs( lhs ) {}
#  ifdef CATCH_CPP11_OR_GREATER
    ExpressionLhs( ExpressionLhs const& ) = default;
    ExpressionLhs( ExpressionLhs && )     = default;
#  endif

    template<typename RhsT>
    ResultBuilder& operator == ( RhsT const& rhs ) {
        return captureExpression<Internal::IsEqualTo>( rhs );
    }

    template<typename RhsT>
    ResultBuilder& operator != ( RhsT const& rhs ) {
        return captureExpression<Internal::IsNotEqualTo>( rhs );
    }

    template<typename RhsT>
    ResultBuilder& operator < ( RhsT const& rhs ) {
        return captureExpression<Internal::IsLessThan>( rhs );
    }

    template<typename RhsT>
    ResultBuilder& operator > ( RhsT const& rhs ) {
        return captureExpression<Internal::IsGreaterThan>( rhs );
    }

    template<typename RhsT>
    ResultBuilder& operator <= ( RhsT const& rhs ) {
        return captureExpression<Internal::IsLessThanOrEqualTo>( rhs );
    }

    template<typename RhsT>
    ResultBuilder& operator >= ( RhsT const& rhs ) {
        return captureExpression<Internal::IsGreaterThanOrEqualTo>( rhs );
    }

    ResultBuilder& operator == ( bool rhs ) {
        return captureExpression<Internal::IsEqualTo>( rhs );
    }

    ResultBuilder& operator != ( bool rhs ) {
        return captureExpression<Internal::IsNotEqualTo>( rhs );
    }

    void endExpression() {
        bool value = m_lhs ? true : false;
        m_rb
            .setLhs( Catch::toString( value ) )
            .setResultType( value )
            .endExpression();
    }

    // Only simple binary expressions are allowed on the LHS.
    // If more complex compositions are required then place the sub expression in parentheses
    template<typename RhsT> STATIC_ASSERT_Expression_Too_Complex_Please_Rewrite_As_Binary_Comparison& operator + ( RhsT const& );
    template<typename RhsT> STATIC_ASSERT_Expression_Too_Complex_Please_Rewrite_As_Binary_Comparison& operator - ( RhsT const& );
    template<typename RhsT> STATIC_ASSERT_Expression_Too_Complex_Please_Rewrite_As_Binary_Comparison& operator / ( RhsT const& );
    template<typename RhsT> STATIC_ASSERT_Expression_Too_Complex_Please_Rewrite_As_Binary_Comparison& operator * ( RhsT const& );
    template<typename RhsT> STATIC_ASSERT_Expression_Too_Complex_Please_Rewrite_As_Binary_Comparison& operator && ( RhsT const& );
    template<typename RhsT> STATIC_ASSERT_Expression_Too_Complex_Please_Rewrite_As_Binary_Comparison& operator || ( RhsT const& );

private:
    template<Internal::Operator Op, typename RhsT>
    ResultBuilder& captureExpression( RhsT const& rhs ) {
        return m_rb
            .setResultType( Internal::compare<Op>( m_lhs, rhs ) )
            .setLhs( Catch::toString( m_lhs ) )
            .setRhs( Catch::toString( rhs ) )
            .setOp( Internal::OperatorTraits<Op>::getName() );
    }

private:
    ResultBuilder& m_rb;
    T m_lhs;
};
(include/internal/catch_expression_lhs.hpp)

長いですが、まずここで注目するのは各種 operator のオーバーロードです。
それぞれ値を受け取り captureExpression 関数を通して ResultBuilder の参照を返しています。

captureExpression では、Internal::compare でオペレーターごとに式の評価が行われ、その結果と左辺・右辺の文字列、オペレーターの文字列が ResultBuilder に設定されます。このように、Catch では operator オーバーロードを利用してそれぞれの情報を集めつつ評価がされていく仕組みになっています。


ちなみに真ん中辺りにある STATIC_ASSERT_Expression_Too_Complex_Please_Rewrite_As_Binary_Comparison& を返すオペレーター(+,-,/,*,&&,||)はこの仕組が使えません。つまり、REQUIRE( a && b ) のような書き方はできませんのでご注意。

他のテスティングフレームワークの対応状況は?
Boost.Test では?
Boost.Test では v1.59 から同様のアサーションが使えるようになってます。
(Catch ではできないオペレーターにも対応しています。)
BOOST_ - 1.59.0
Boost 1.59.0がリリースされました - Faith and Brave - C++で遊ぼう

Google Test では?
Google Test には残念ながらそのようなアサーションはありません。

iutest では?
iutest は私が作っているテスティングフレームワークです。 https://github.com/srz-zumix/iutest
iutest でも同様の記述ができ、これも operator ->* のオーバーロードで実現させています。実装はだいたい似たような感じですが、iutest では Catch では使えないオペレーターにも対応してます。

int f(void)
{
    return 42;
}

IUTEST(ExpressionTest, Test)
{
    IUTEST_EXPECT(f() + f() == 2);
}




最後に
Catch にしろ、Boost.Test にしろ、iutest にしろ、所謂 PowerAssert は便利な機能です。
普段 Google Test をよく使っているので、これがないとテストが書けないわけではありませんが、ASSERT_EQ(a, b) などと書くよりも ASSERT( a == b ) と書けたほうが、より直観的でわかりやすいですよね。

Google Test も(まだまだ開発進んでいるので)対応することを期待しつつ、これで締めたいと思います。
C++ Advent Calendar 2015 10日目は hmito さんで「[C++] csvファイル入出力用のiteratorを作ってみた話 - Qiita」です。

0 件のコメント:

コメントを投稿