2012年1月31日火曜日

ASSERT_EQ(NULL, ptr) がコンパイルエラーにならない理由

社内テストフレームワーク用に、ASSERT_EQ を実装した時のお話です。

これまでのテストは、 ASSERT のみで記述されていました。
しかし、それではログから必要な情報を十分に得ることができなかったため、
比較マクロを追加することにしました。

まず、書いたのが以下のようなコード
#define COMPARE_HELPER(op_name, op) \
template<typename T1, typename T2>  \
static AssertionResult CmpHelper##op_name( \
            const char* expr1, const char* expr2  \
            , const T1& val1, const T2& val2) { \
    if( val1 op val2 ) return AssertionResult::Success();   \
    else {  \
        return AssertionResult::Failure() \
            << "error: Expected: " << expr1 << " " #op " " << expr2   \
            << "\n  Actual: " << val1 << " vs " << val2; \
    }   \
}

COMPARE_HELPER(EQ, ==)
COMPARE_HELPER(NE, !=)
COMPARE_HELPER(LE, <=)
COMPARE_HELPER(LT, < )
COMPARE_HELPER(GE, >=)
COMPARE_HELPER(GT, > )
比較テストマクロの部分は省いてありますが、
ASSERT_EQ(expected, actual) を書くと、CmpHelperEQ 関数が呼ばれるようになってます。

簡単簡単と意気揚々とテストを書くわけですが、これだけではダメです。

TEST(Hoge, foge)
{
    int* p = NULL;
    // error C2040: '==' : 'const int' は 'int *const ' と間接操作のレベルが異なります。
    ASSERT_EQ(NULL, p);
}
ASSERT_EQ(NULL, p) は、CmpHelperEQ<int, int*> と解釈されるため、エラーになってしまいます。
※gtest ではエラーになりません。

では、なぜ gtest ではエラーにならないのか、調べてみましょう。
(※アバウトなことしか書いていないです。ご了承ください。)

NULL リテラルか判断する
gtest の ASSERT_EQ マクロの定義を見てみると以下のようになっています。
#if !GTEST_DONT_DEFINE_ASSERT_EQ
# define ASSERT_EQ(val1, val2) GTEST_ASSERT_EQ(val1, val2)
#endif

#define GTEST_ASSERT_EQ(expected, actual) \
  ASSERT_PRED_FORMAT2(::testing::internal:: \
                      EqHelper<GTEST_IS_NULL_LITERAL_(expected)>::Compare, \
                      expected, actual)
他の ASSERT_* マクロとは、少し違っています。
まず、 EqHelper と GTEST_IS_NULL_LITERAL_ が目に入ります。
GTEST_IS_NULL_LITERAL_
見るからに NULL と関係がありそうです。

class Secret;
char IsNullLiteralHelper(Secret* p);  // A
char (&IsNullLiteralHelper(...))[2];  // B

# define GTEST_IS_NULL_LITERAL_(x) \
    (sizeof(::testing::internal::IsNullLiteralHelper(x)) == 1)
コメントなど省いてありますが、 GTEST_IS_NULL_LITERAL_ の実装はこうです。
Secret* に変換できるならば A
それ以外は、B として解釈されます。

すると、
GTEST_IS_NULL_LITERAL_(NULL) は、true です。

スバラシイ!!でも、まだです。

以下は、EqHelper の GTEST_IS_NULL_LITERAL_ が true の場合の実装です。
template <>
class EqHelper<true> {
 public:
  template <typename T1, typename T2>
  static AssertionResult Compare(
      const char* expected_expression,
      const char* actual_expression,
      const T1& expected,
      const T2& actual,
      typename EnableIf<!is_pointer<T2>::value>::type* = 0) {
         // 第二引数がポインター型でない
         // 略
      }
  template <typename T>
  static AssertionResult Compare(
      const char* expected_expression,
      const char* actual_expression,
      Secret* /* expected (NULL) */,
      T* actual) {
    return CmpHelperEQ(expected_expression, actual_expression,
                       static_cast<T*>(NULL), actual);
  }
};
ASSERT_EQ の第二引数がポインター型であることをチェックしています。
ポインターであれば、2番目の関数が呼ばれます。
2番目の関数では、比較対象のポインター型にキャストしてますので、エラーにならないという寸法です。


いやー、非常に勉強になりますね。

0 件のコメント:

コメントを投稿