它在帮助我们在定义一个测试方法的时候自动注册到 test_funcs_ 中。原理其实很简单,就是在声明一个函数的同时声明一个 Test 全局变量,将定义的测试方法传入,这个测试函数就会在 Test 的构造函数中被插入 test_funcs_ 。
所以我们只要这样编写测试代码,就能实现自动注册了:
1 2 3
TEST_FUNC(testSomething){ ... }
看起来这个方法不错是吧?可惜这种方法是有问题的!至少在我的 vs2013 上会崩溃!
问题就出在 Test 全局变量和 Test::test_funcs_ 的初始化顺序上。你无法保证 Test::test_funcs_ 比全局变量 Test 先初始化。很奇怪是吧? Test 的静态成员变量居然比 Test 全局变量的初始化时间晚,也就是说在 Test 这个类还没有完全准备好的时候,就已经拿来创建一个全局变量了。书上一直强调的全局变量的初始化顺序不能确定难道也有这种含义?
一种可能可行的方法
既然是因为初始化顺序导致了内存错误,那我们只要使用某种机制让保存测试函数的容器首先初始化就行了。
让我们将 Test 类的定义修改成下面的样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
classTest{ public: virtualvoidrun()= 0;
staticvoidrunAllTest(){ for (auto i : test_list){ i->run(); } }
容器里面不再直接放测试函数,改为放 Test 的指针。而 Test 又是一个抽象类,所以事实上放的是 Test 的子类。
再把 TEST_FUNC 宏的定义改成下面的样子:
1 2 3 4 5 6 7 8 9 10 11 12 13
#define TEST_FUNC(NAME) \ class NAME : public Test{ \ public: \ virtual void run(); \ private: \ NAME(){ \ addTest(this); \ } \ static NAME* instance_; \ }; \ NAME* NAME::instance_ = new NAME(); \ void NAME::run()
现在实际上用户写的测试方法实现的是 Test 的子类的 run 方法。
依然是需要在定义测试方法的时候顺便定义一个全局变量,但我们换了一种方式,定义了一个类静态变量。子类在构造函数中把自己注册到 Test 的测试容器中,而且子类还包含了一个本类指针静态成员变量(有点拗口,但看代码很容易看出来)。在子类的静态成员变量初始化的之前,父类的静态成员变量应该就已经初始化了。就是根据这种机制,达到了我们的目的。
template <typename T> TypeId GetTypeId(){ // The compiler is required to allocate a different // TypeIdHelper<T>::dummy_ variable for each T used to instantiate // the template. Therefore, the address of dummy_ is guaranteed to // be unique. return &(TypeIdHelper<T>::dummy_); }
...
template <typename T> classTypeIdHelper { public: // dummy_ must not have a const type. Otherwise an overly eager // compiler (e.g. MSVC 7.1 & 8.0) may try to merge // TypeIdHelper<T>::dummy_ for different Ts as an "optimization". staticbool dummy_; };
这里直接用一个类的静态成员变量的地址当作 id 号。当时我就懵逼了,明明很简单,怎么就感觉那么玄幻呢?
//gtest.cc UnitTest* UnitTest::GetInstance(){ // When compiled with MSVC 7.1 in optimized mode, destroying the // UnitTest object upon exiting the program messes up the exit code, // causing successful tests to appear failed. We have to use a // different implementation in this case to bypass the compiler bug. // This implementation makes the compiler happy, at the cost of // leaking the UnitTest object.
// CodeGear C++Builder insists on a public destructor for the // default implementation. Use this implementation to keep good OO // design with private destructor.
//gtest-internal-inl.h classGTEST_API_ UnitTestImpl { ... voidAddTestInfo(Test::SetUpTestCaseFunc set_up_tc, Test::TearDownTestCaseFunc tear_down_tc, TestInfo* test_info){ // In order to support thread-safe death tests, we need to // remember the original working directory when the test program // was first invoked. We cannot do this in RUN_ALL_TESTS(), as // the user may have changed the current directory before calling // RUN_ALL_TESTS(). Therefore we capture the current directory in // AddTestInfo(), which is called to register a TEST or TEST_F // before main() is reached. if (original_working_dir_.IsEmpty()) { original_working_dir_.Set(FilePath::GetCurrentDir()); GTEST_CHECK_(!original_working_dir_.IsEmpty()) << "Failed to get the current working directory."; }
//gtest.cc UnitTest* UnitTest::GetInstance(){ // When compiled with MSVC 7.1 in optimized mode, destroying the // UnitTest object upon exiting the program messes up the exit code, // causing successful tests to appear failed. We have to use a // different implementation in this case to bypass the compiler bug. // This implementation makes the compiler happy, at the cost of // leaking the UnitTest object.
// CodeGear C++Builder insists on a public destructor for the // default implementation. Use this implementation to keep good OO // design with private destructor.