无论你是否采用测试驱动开发(TDD),编写Unit test case都是重要的工作.在项目的各个阶段,我们都需要构建测试、运行测试、报告测试结果. 有很多框架为这个工作提供便利,比如大名鼎鼎的xUnit系列.今天,我准备介绍的是一个轻量级的C++ Unit Test框架:
TUT (Template Unit Tests的缩写)
大家第一个想问的问题估计就是:它和CppUnit有什么不同? 它有什么特点?
我觉得最关键的地方在于TUT是一个轻量级的框架,TUT是使用template技术写的,完全没有macro. 除去例子,它的代码一共只有两个头文件.我们完全不用build这个框架然后设置编译连接那么多麻烦,只要在编写Unit test的时候,把这两个头文件拷贝到项目目录或者设置一下头文件的包含路径就可以了.
TUT把Unit test case通过group-test的层次组织,用户可以把相似的test case放到同一个group,这个group有一个唯一的名字, group包含多个test, 包含的test数量取决于你的编译器允许的template嵌套深度.用户可以选择运行所有的
测试或者只是部分group,甚至部分test. 在每个test里面,你可以使用TUT提供的一些方法来确认
测试结果的正确性,告诉TUT这个
测试是成功还是失败.
闲话少说,估计大家喜欢眼见为实,让我们写个例子来看看:
1. 首先包含必要的头文件#include <stdio.h>
#include "tut.h"
#include "tut_reporter.h"
2. 接着写了一个测试class
//测试类,做的事情很简单,记住给它的最大的数字,我们就测试这个类
class max_pool
{
int m_max;
public:
max_pool(int x=5) : m_max(x){ printf("%s init max num %d\n", __func__, x); }
void try_set_value(int num) { if (num>m_max) m_max = num; }
int get_max() { return m_max; }
};
3. 写关于test group的代码 /*这个类用在test_basic里面,
我们可以用它来完成一些每个test都需要做的初始化和清理工作,
不过这里只是为了说明, 只是输出点文字
*/
class obj_init
{
public:
obj_init() { printf( "obj_init for each test\n" ); }
~obj_init() { printf( "~obj clean for each test\n" ); }
};
namespace tut
{
struct test_basic
{
max_pool m;
obj_init xxx;
};
//每个测试用例,都会重新构造和析构test_basic里面的m 和 xxx,
//假如有什么需要每个测试都需要初始化和销毁的,放在这里
typedef test_group<test_basic> factory;
typedef factory::object object;
factory tf("max_poll_test_name");//这里设置了测试group的名字
}
4. 写两个test case
namespace tut
{
/**
* Checks insert operation
*/
template<>
template<>
void object::test<1>()
{
printf("\trun test case <1>\n");
m.try_set_value(4);
ensure( m.get_max()==5 );//告诉TUT假如这里==5就是测试OK
}
template<>
template<>
void object::test<2>()
{
printf("\trun test case <2>\n");
m.try_set_value(8);
ensure( m.get_max()==8 );//告诉TUT假如这里==8就是测试OK
}
}
5.终于写到main()了,运行所有测试:
using tut::reporter;
using tut::groupnames;
namespace tut
{
test_runner_singleton runner;
}
int main()
{
reporter visi;
tut::runner.get().set_callback(&visi);
tut::runner.get().run_tests();
return 0;
}
编译...结果我用的EditPlus+cygwin组合编译出现了问题,说我的template嵌套层次太多。看看tut.h:
template <class Data, int MaxTestsInGroup = 50>
class test_group : public group_base { ...
原来是 test_group<test_basic> 缺省有50个test,也就是说编译器需要能支持50层的template嵌套,
我修改了一下typedef test_group<test_basic,15> factory; 这样编译器只要能支持15层嵌套就可以通过了。
大家注意,VC6也是无法兼容TUT的,需要VS2003或者以上版本的支持.
最后运行的结果当然是测试通过了,结果如下, 大家可以注意到struct test_basic里面对象在每个test的输出:
max_pool init max num 5obj_init for each test
run test case <1>~obj clean for each test
max_poll_test_name: .max_pool init max num 5
obj_init for each test
run test case <2>
~obj clean for each test.max_pool init max num 5
obj_init for each test
~obj clean for each test
by http://blog.joycode.com/peon/archive/2007/04/03/100307.aspx
所谓原子操作,就是"不可中断的一个或一系列操作" , 在确认一个操作是原子的情况下,多线程环境里面,我们可以避免仅仅为保护这个操作在外围加上性能昂贵的锁,甚至借助于原子操作,我们可以实现互斥锁。
很多操作系统都为int类型提供了+-赋值的原子操作版本,比如 NT 提供了 InterlockedExchange 等API, Linux/UNIX也提供了atomic_set 等函数。
前两天有同学问我:在x86上,g_count++ (int类型) 是否是一个原子操作? 我的回答是"不是的, 多个CPU的机器(SMP)上面这就不是原子操作"。
今天想起,在单CPU上这个是否是原子操作呢,但是这个和编译器有关,编译器可能有两种编译方式:
A. 多条指令版本 , 这就不是原子的
MOV 寄存器 , g_count
ADD 寄存器, 1
MOV g_count , 寄存器
B. 单指令版本, 这在单CPU的x86上就是原子的
INC g_count
只能写程序验证了, 让5个线程每个对 g_count++ 一亿次,假如是原子操作的话,结果应该是5亿:
其实还需要对 g_count 进行volatile声明,防止编译器对这里不适当的优化,为了看看编译器对volatile的处理,我另外做了个volatile版本作为比较。
#include <windows.h>
#include <stdio.h>
int g_count = 0;
DWORD WINAPI ThreadFunc( LPVOID lpParam )
{
int i;
printf( "Thread %d start\n", (DWORD*)lpParam );
for (i=0; i <100000000 ; i++)
g_count++;
printf( "Thread %d quit\n", (DWORD*)lpParam );
return 0;
}
#define THREAD_NUM 5
VOID main( VOID )
{
DWORD dwThreadId;
HANDLE hThread;
int i;
for (i=0;i<THREAD_NUM;i++)
{
hThread = CreateThread(
NULL, // default security attributes
0, // use default stack size
ThreadFunc, // thread function
(LPVOID)i, // argument to thread function
0, // use default creation flags
&dwThreadId); // returns the thread identifier
// Check the return value for success.
if (hThread == NULL)
{
printf( "CreateThread failed.\n" );
}
}
printf("Press any key after all thread exit...\n");
getchar();
printf("g_count %d\n", g_count);
if (g_count!=THREAD_NUM*100000000)
{
printf("ERROR! g_count %d!=%d\n", g_count, THREAD_NUM*100000000);
}
getchar();
//一个随手的程序,就不close handle了
}
volatile的本意是易变的, 它限制编译器的优化,因为CPU对寄存器处理比内存快很多,我想这个程序的没有加上volatile的版本优化以后应该是这样:
MOV 寄存器, g_count
for循环一亿次, 执行 INC 寄存器
MOV g_count, 寄存器
这样,最后g_count的值应该是1亿,2亿,3亿,4亿,5亿的整数,1亿出现的可能性较高。
而加上volatile以后,或者是没有代码优化的版本,都是老老实实对内存加上一亿次,假如不是原子操作的话,最后结果就会比五亿小。
用的是Vc6的cl编译器,我预期的结果是这样的:
++是原子操作
|
没有代码优化
|
代码优化(cl -O2编译)
|
没有 volatile
|
g_count == 五亿 |
g_count的值应该是1亿,2亿,3亿,4亿,5亿的整数 |
volatile
|
g_count == 五亿 |
g_count == 五亿 |
++ 不是原子操作
|
没有代码优化
|
代码优化(cl -O2编译) |
没有 volatile
|
g_count < 五亿 |
g_count的值应该是1亿,2亿,3亿,4亿,5亿的整数,1亿出现的可能性较高 |
volatile
|
同上 |
g_count < 五亿 |
但是最后的结果却让我大跌了一下眼镜:
VC6实验的结果
|
没有代码优化
|
代码优化
|
没有 volatile
|
g_count 一般为五亿, 偶尔< 五亿(疑惑中...) |
都是五亿(疑惑中...) |
volatile
|
同上(疑惑中...) |
g_count = < 五亿(这个可以解释) |
这个结果太让人疑惑了,没办法,只能看asm代码了, 首先看看为什么
volatile的版本为什么和预期不符合吧:
-
这里是没有优化的版本(编译命令行 cl -Fa test_thread.c):
for (i=0; i <100000000 ; i++)
初始化i=0; mov DWORD PTR _i$[ebp], 0
jmp SHORT $L52751
$L52752: i++ mov ecx, DWORD PTR _i$[ebp]
add ecx, 1
mov DWORD PTR _i$[ebp], ecx
$L52751: 判断 i <100000000 cmp DWORD PTR _i$[ebp], 100000000 ; 05f5e100H
jge SHORT $L52753
g_count++;
//这里发现编译使用的是多个指令,也就是说g_count++不是原子的 mov edx, DWORD PTR _g_count
add edx, 1
mov DWORD PTR _g_count, edx
jmp SHORT $L52752
-
下面是加了volatile的优化版本(编译命令行 cl -Fa test_thread.c -O2)
//初始化 i = 100000000, 这个循环变量被直接放到了寄存器里面 mov eax, 100000000 ; 05f5e100H
$L52793:
//g_count++;这里发现编译使用的是多个指令,也就是说g_count++不是原子的 mov ecx, DWORD PTR _g_count
inc ecx
mov DWORD PTR _g_count, ecx
//下面又是循环体的asm代码 dec eax
// i-- jne SHORT $L52793
// if (i>0) 则继续循环
终于发现了问题所在了, 优化以后,循环从i++变成了i--, 就是如下的形式:
for (i=100000000; i >0 ; i--)
g_count++;
因为将一个数字和0比较和将其与其他数字比较更加有效率优势,而且这里i在循环体里面并不使用,所以VC编译器将其变换成上面的形式,可以大大节省循环运行的时钟周期。
这样,未优化的版本有很大的机会出现 g_count == 五亿 就有了解释,是因为:
- CPU对于纯粹的整数运算是很快的,一亿次循环里面,可能只有一两次的线程上下文切换
- 没有优化的版本循环体比++操作本身更加耗时,这样切换操作很可能出现在 for 循环中, 而不是 g_count++ 的三条指令之间
这里也证明了VC6编译器对于 ++ 的运行代码是是非原子的,查了一下资料 这3条指令在pentium以后的CPU比一条inc更快
发现汇编代码的循环体完全没有了:
mov eax, DWORD PTR _g_count
push esi
add eax, 100000000 ; 05f5e100H
表示成C的代码大概就是这样: g_count+=100000000; 编译器还是很聪明,发现这个循环其实使用前面的语句也可以达到目的,干脆把循环拿掉了,这样因为线程执行时间很短,往往一个线程都执行完了其他线程还没有被调度,所以结果都是5亿了。
附带以下总结:
1. 不要小看编译器的聪明程度,上面的那些优化,我在gcc上也作了验证,我们不要太在意i++/++i之类的优化,要相信编译器能做好它
2. ++的操作在单CPU的x86上也不是原子性的,所以优化多线程性能的兄弟不要在这里搞过火,老实用 InterlockedIncrement 吧
3. x86上,不管是否SMP, 对于int(要求地址4 bytes对齐)的读取和赋值还是原子的,不过这个就和这个试验无关了(RISC的机器就不要这样做了,大家还是加锁吧)
顺便推荐一下我在看的历史小说连载: 《明朝的那些事儿-历史应该可以写得好看》 http://blog.sina.com.cn/dangnianmingyue , 算是作为一个普通读者对作者当年明月的支持: 虽然网络暴力无处不在,可是你的读者也无处不在!
另一个推荐的是呆伯特漫画,这里有中文版本:

参考: http://www.codeproject.com/atl/atlserver.asp
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vclib/html/vclrfATLServerReference.asp
ATL7中除了制作COM和windows组件的大量的工具,还提供了大量的工具类,可以让我们在C++编程方面事半功倍。
微软在MFC中提供的大都是GUI的工具类,对于组件设计和一些服务的编程,未免有力有不逮的地方,ATL除了作为一个FrameWork作为MFC的补充外,还有很多可以单独抽出来使用的工具类,大大方便了我们的编程工作, 特别是在ATL7中,增加了很多这样的工具类,让我们瞧瞧:
1.首先要说的就是CString了,大家对她的感情自然不必多说,以往只有MFC才可以使用未免让人觉得可惜,甚至成为一些人在ATL组件中包含MFC的理由,现在也包含在ATL中了,只要#include? 〈atlstr.h〉就可以了。同样的可以在ATL中使用的还有CSize , CRect ,CPoint 等等。
2.CRegKey,操作注册表的好东东,因为在ATL3中就有了,大家也都很熟悉,就不多介绍了 。
2.HASH工具 - 下面是一个MD5代码:要包含atlcrypt.h头文件
void TestMd5Hash()
{
?const char* szPassword = "real9video.rm?ts=2003-12-23-16-23-9";
?CCryptProv prov;
?
?HRESULT hr = prov.Initialize();
?if( hr == 0x8009016L )
???prov.Initialize( PROV_RSA_FULL,NULL,MS_DEF_PROV,CRYPT_NEWKEYSET );
?//这个其实和下面的代码差不多?
?//CCryptKeyedHash? hash;
?//hash.Initialize(prov, CALG_MD5, CCryptKey::EmptyKey, 0);
?CCryptMD5Hash hash;
?hash.Initialize( prov );
?hash.AddString( szPassword );
?
?BYTE buf[1024];
?DWORD outLen;
?hash.GetValue( buf , &outLen? );
?printHex( buf , outLen );//这个大家自己实现,打印byte数组的内容
//应该是 BC CB 76 69 78 CF AB 4B 8 D E9 42 32 B0 88 9C
}
3.编码装换,下面是Base64和UTF8的转换,要包含atlenc.h
void Base64()
{
?CString sSource = "some string";
?int nDestLen = Base64EncodeGetRequiredLength(sSource.GetLength());
?CString str64;
?Base64Encode((const BYTE*)(LPCSTR)sSource, sSource.GetLength(),str64.GetBuffer(nDestLen), &nDestLen);
?str64.ReleaseBuffer(nDestLen);
?cout<<(LPCSTR)str64;
?int nDecLen = Base64DecodeGetRequiredLength(nDestLen);
?CString strOrig;
?Base64Decode(str64, str64.GetLength(), (BYTE*)strOrig.GetBuffer(nDecLen),????????????? &nDecLen);
?strOrig.ReleaseBuffer(nDecLen);
?cout<<(LPCSTR)strOrig<}
void UTF8Convert()
{
?char buf[128];
?memset( buf , 0 , 128 );
?int n = AtlUnicodeToUTF8( L"复件" , 2 , buf , 128 );
?printHex( (BYTE*)buf ,n );
}
4. CATLRegExp 正则表达式工具,msdn中有例子,这里也贴一下
CAtlRegExp<> regexp;
CAtlREMatchContext<> mc;
if(regexp.Parse("^\\d+-\\d+$") == REPARSE_ERROR_OK)
{
??? const char* szNumDashNum="5663-4662";
??? if(regexp.Match(szNumDashNum, &mc)) { ATLTRACE("Matched");??? }
}
待续...