• About
    • Resume
A Game Developer also plays some guitar

Category Archives: C++

Game Framework的两种实现方式

April 24, 2011 2:11 pm / Leave a Comment / Benny Chen

一年多前,曾经写过一篇关于Game Engine Framework的文章,当时基本上是为了巩固并加深对framework的理解。最近又做了一些关于framework的工作,对于framework的实现方式又有了些新的认识。虽然我现在做的已经完全不是game了,不过方式对于game也同样适用。

这篇文章主要希望通过一些示例性的C++代码介绍game framework的两种实现方式。首先,我还是搬出一年多前的那篇文章里的game流程图,以下的一些代码也主要基于这张图实现。对于图的细节在这里不再赘述,可以再去翻看之前的那篇文章。

FoC of Game

1.通过继承

这是一种最传统的方式了,之前我一直使用这种方式。基本上是提供一个基类,基类封装并决定了整个程序控制流,同时基于该控制流,基类提供了一系列的接口(在C++里是虚函数),以供继承类override,并实现定制化的行为。就像下面的这个Game类,run函数决定了整个程序的执行逻辑,同时initialize,update,render等非纯虚函数则提供了接口以供用户定制并实现具体Game想要的行为。

// 通过继承来实现的framework
class Game
{
public:
	void run()
	{
		m_isQuit = false;
		initialize();
		while ( !m_isQuit ) 
		{
			handleInput();
			update();
			render();
		}
		destroy();
	}
	
protected:
	virtual void initialize() {}
	virtual void update() {}
	virtual void render() {}
	virtual void destroy() {}
	virtual void onMouse( Mouse mouse ) {}
	virtual void onKeyboard( Keyboard keyboard ) {}
	
private:
	void handleInput()
	{
		Event e;
		while ( popEvent( &e )  )
		{
			switch ( e.type ) 
			{
			case EVENT_QUIT:
				m_isQuit = true;
				break;
			case EVENT_MOUSE;
				onMouse( e.mouse );
				break;
			case EVENT_KEYBOARD:
				onKeyboard( e.keyboard );
				break;
			default:
				break;
			}
		}
	}
	
private:
	bool m_isQuit;
};

在上面的代码中,我把实现全部写在了类的定义头文件中,在这里只是为了省事,在现实中你最好还是分开在.h和.cpp文件中。而且在这里,Game类还可以做的更多,我这里只是为了说明实现方式,所以依照前面的流程图而尽量让它简单。另外,不要纠结这段代码中的Event,Mouse,Keyboard等几个类和popEvent方法,它们只是我为了将故事说得更圆满一点而假象出来的几个类,我想你应该能猜到它们是用来干嘛的。

基于此framework,一个具体的游戏只需要重写这些虚函数就可以了,像下面的这些代码。

class ConcreteGame : public Game
{
private:
	void initialize()
	{
		// do some real initialization
	}
	
	void update()
	{
		// do some real update
	}
	
	void render()
	{
		// do some real rendering
	}

	void destroy()
	{
		// do some real destroy
	}
	
	void onMouse( Mouse mouse )
	{
		// handle mouse event
	}
	
	void onKeyboard( Keyboard keyboard )
	{
		// handle keyboard event
	}
};

int main()
{
	ConcreteGame mygame;
	mygame.run();
	return 0;
}

2.通过Delegation模式

前一种方法有一个缺点,就是将程序的控制流和具体行为紧耦合在了一起,而且还必须使用继承,不易于扩展。现代软件设计的一些方法告诉我们,要尽量使用接口,且尽量使用组合而非继承。Delegation模式就可以帮我们达到这一目的。

何为Delegation模式,wiki上的解释一语中的:

Delegation is the simple yet powerful concept of handing a task over to another part of the program.

Delegation将一些task委托给程序的另外一部分来处理,以达到了行为使用者和具体行为的松耦合。

以下是通过Delegation模式重新实现的framework。

// 通过Delegation模式来实现的framework
class GameDelegation
{
public:
	virtual void initialize() {}
	virtual void update() {}
	virtual void render() {}
	virtual void destroy() {}
	virtual void onMouse( Mouse mouse ) {}
	virtual void onKeyboard( Keyboard keyboard ) {}
};

class Game
{
public:
	Game( GameDelegation *gameDelegation )
	{
		m_gameDelegation = gameDelegation;
	}
	
	void run()
	{
		m_isQuit = false;
		if ( m_gameDelegation == NULL )
		{
			return;
		}
		
		m_gameDelegation->initialize();
		while ( !m_isQuit ) 
		{
			handleInput();
			m_gameDelegation->update();
			m_gameDelegation->render();
		}
	}
	
private:
	void handleInput()
	{
		Event e;
		while ( popEvent( &e ) )
		{
			switch ( e.type ) 
			{
			case EVENT_QUIT:
				m_isQuit = true;
				break;
			case EVENT_MOUSE;
				m_gameDelegation->onMouse( e.mouse );
				break;
			case EVENT_KEYBOARD:
				m_gameDelegation->onKeyboard( e.keyboard );
				break;
			default:
				break;
			}
		}
	}
	
private:
	bool m_isQuit;
	GameDelegation *m_gameDelegation;
};

基于此framework,当需要具体实现一个游戏的时候,只需要实现GameDelegation接口即可,然后将Game类的GameDelegation设置为你所实现的具体的ConcreteGameDelegation类,代码如下。

class ConcreteGameDelegation : public GameDelegation
{
public:
	void initialize()
	{
		// do some real initialization
	}
	
	void update()
	{
		// do some real update
	}
	
	void render()
	{
		// do some real rendering
	}

	void destroy()
	{
		// do some real destroy
	}
	
	void onMouse( Mouse mouse )
	{
		// handle mouse event
	}
	
	void onKeyboard( Keyboard keyboard )
	{
		// handle keyboard event
	}
};

int main()
{
	ConcreteGameDelegation myGameDelegation;
	Game mygame( &myGameDelegation );
	mygame.run();
	return 0;
}

最近我正好做了一些iOS上开发的研究,发现Delegation在iOS框架中被普遍使用。比如,iOS中的每个应用程序对应的是一个UIApplication类,为每一个UIApplication,开发人员必须要实现一个特定的UIApplicationDelegate,并将它指定给当前的应用程序(在main函数中通过UIApplicationMain函数指定,或者是在nib文件中绑定)。在这个UIApplicationDelegate类中,开发人员就需要重写诸如didFinishLaunchingWithOptions,applicationWillTerminate这样的方法,就类似与上面game framework中的initialize,destroy等方法。当然iOS框架要复杂很多,它还用到其它一系列的设计模式,有空研究些这样设计是非常有趣的。

图片来自于这篇苹果官方关于iOS中delegation的介绍:http://developer.apple.com/library/ios/#documentation/General/Conceptual/DevPedia-CocoaCore/Delegation.html

Posted in: C++, Game Programming, iOS / Tagged: C++, delegation, framework, game, 实现, 继承

Lua相关问题整理(3)

November 27, 2010 5:04 pm / Leave a Comment / Benny Chen
  1. 在注册给Lua的C函数中为Lua提供默认参数

    使用luaL_optstring, luaL_optnumber, luaL_optinteger等Lua API,如下示例,函数有一个默认字符串参数,默认值为””,这样在Lua中调用whatever的时候,whatever()或者whatever( “whatever”)均可。(Oh…whatever…随便…都行…)

    // C code
    int Whatever( lua_State *L )
    {
    	string str = luaL_optstring( L, 1, "" );
    	//...omitted code...
    }
    lua_register( L, "whatever", Whatever );
    
  2. 建立Lua字符串到C enum的映射

  3. 使用luaL_checkoption这个Lua API,它可以把从Lua传来的string转换为相应的C string array中的index,从而可以建立Lua字符串和C enum的映射。以下是个简单的示例:

    // C code
    enum PlayerType
    {
    	PLAYER_TYPE_UNDEFINED = -1,
    	PLAYER_TYPE_KING = 0, // 主公
    	PLAYER_TYPE_INSURGENT, // 反贼
    	PLAYER_TYPE_LOYAL, // 忠臣
    	PLAYER_TYPE_TREACHEROUS, //内奸-_^
    	NUM_PLAYER_TYPE // just a sentinel
    };
    
    const char * const PlayerTypeList[NUM_PLAYER_TYPE + 1] =
    {
    	"KING",
    	"INSURGENT",
    	"LOYAL",
    	"TREACHEROUS",
    	NULL
    };
    
    static int testPlayerType( lua_State *L )
    {
    	PlayerType type = static_cast< PlayerType >( 
    						luaL_checkoption( L, 1, 
    						"INSURGENT", PlayerTypeList ) );
    	std::cout << "Type index is " << type 
    		      << " - " << PlayerTypeList[type] 
    		      << std::endl;
    	return 0;
    }
    
    lua_register( L, "setPlayerType", setPlayerType )
    

    首先enum PlayerType定义了一组角色类型,来自人人都爱的三国杀:-)。

    接着PlayerTypeList定义了一个字符串数组,给Lua使用。注意需要保证enum和字符串数组的对应,比如PlayerTypeList[PLAYER_TYPE_KING]是“KING”,同时,PlayerTypeList必须以NULL结尾。

    在定义给Lua的函数testPlayerType中,就可以用luaL_checkoption将Lua传来的字符串参数转换为相应enum的值。luaL_checkoption还支持默认参数,比如在上面例子中,将第三个参数设为“INSURGENT”,如果Lua中没有提供任何参数,则PlayerType就为与“INSURGENT”相对应的PLAYER_TYPE_INSURGENT。

    以下是一组测试及结果:

    --Lua code
    testPlayerType( "KING" ) -- Type index is 0 - KING
    testPlayerType() -- Type index is 1 - INSURGENT
    testPlayerType( "whatever" ) -- bad argument #1 to 'testPlayerType' (invalid option 'whatever')
    
  4. 在Lua中实现eval函数

    众所周知,JavaScript中有一个著名的eval函数,它用于把一个字符串当作一段JS代码去执行,在Lua中没有提供类似的函数,但稍微包装下Lua的库函数loadstring即可实现,以下是代码。

    --Lua code
    function eval( str )
    	local func = loadstring( "return " ..str );
    	return func()
    end
    

    这样已经可以了,不过相比于JS的eval函数,功能稍微差一些,因为它不支持赋值语句,这是Lua语言天然的原因,因为Lua的赋值运算符没有返回值,在其他语言中常见传递赋值的“i=j=1”(先赋值j=1,然后将(j=1)的返回值j赋值给i),在Lua中是不允许的。所以当eval执行的是赋值运算(比如i=1)的时候,return i=1就会出错。

    下面是一些测试例子:

    --Lua code
    print( eval( "3+4" ) ) -- OK, 打印7
    
    function Multiply( a, b )
    	return a*b
    end
    
    print( eval( "Multiply( 3, 4 )" ) ) -- OK,打印12
    
    print( eval( "i = 1" ) ) -- 错误, attempt to call a nil value
    i = 1
    print( eval( "i" ) ) -- OK,打印1
    print( eval( "i = i + 1" ) ) -- 错误, attempt to call a nil value
    
  5. 实现luaL_checkbool

    不知道为什么Lua的API没有提供luaL_checkbook函数,不过很容易实现:

    // C code
    BOOL luaL_checkbool( lua_State *luaVM, int numArg )
    {
    	BOOL b = FALSE;
    	if ( lua_type( L, numArg ) == LUA_TBOOLEAN )
    	{
    		b = lua_toboolean( L, numArg );
    	}
    	else
    	{
    		luaL_typerror( L, numArg, lua_typename( L, LUA_TBOOLEAN ) )
    	}
    	return b;
    }
    
Posted in: C++, Game Programming, Lua / Tagged: enum, eval, lua, luaL_checkbool, 默认参数

Lua相关问题整理(2)- 如何在C中为Lua提供同步调用接口

November 7, 2010 10:25 pm / Leave a Comment / Benny Chen

这个问题的具体描述是——C注册给Lua一个函数,但Lua调用该C函数并不能立即获得结果(比如需要访问远程服务器获取值),如何能让Lua停止并等待,直到获取到结果后,才继续执行接下来的脚本。

举个例子,比如说有一个C函数login,我们试图通过调用该函数以执行用户的登录操作并获取验证结果。首先看下面这段C代码:

// C code
int login( lua_State *L )
{
	string user = luaL_checkstring( L, 1 );
	string password = luaL_checkstring( L, 2 );

	// 该函数将user和password发送到服务器后立即返回,
	// 绝不要在此处阻塞,这将严重影响效率
	sendAuthenticationInfo( user, password );

	return 0;
}
// 注册给Lua
lua_register( L, "login", login );

可以看到,因为该函数需要访问远程的登录服务器,在系统中一般都采取异步操作(让系统阻塞等待结果是不可接受的)。但是这样Lua开发人员调用login函数时,也只能异步等待结果,下面是Lua中处理登录操作的代码。

-- Lua code
-- 当用户点击“登陆”按钮后,执行该函数
function onClickLoginBtn()
	local user = ...
	local password = ...
	login( user, password ) -- 只是发送login命令
end

--当获取login结果后的回调函数
function onGetLoginResult( bPass )
	if bPass == true then
		print( "Authentication succeeded." )
		...omitted code...
	else
		print( "Authentication failed." )
		...omitted code...
	end
end

这段Lua代码很好理解,首先onClickLoginBtn是一个按钮事件处理函数,当用户点击了”登录“按钮后,会触发该函数。该函数首先从界面上获取用户输入的user和password,然后调用了上面C中所定义的login函数。正如前面的C代码所写的,login函数不会马上得到认证结果,所以执行后马上退出。另一个函数是onGetLoginResult,这个Lua函数需要当C系统中获取到认证结果后被回调执行,以真正执行login的后续操作。所以,我们还需要在C中添加回调的代码。

// C code
// 当服务器端返回认证结果后
BOOL loginResult = ...
lua_getglobal( L, "onGetLoginResult" );
lua_pushboolean( L, loginResult );
lua_call( L, 1, 0 );

在这里,我们在C中hardcode了回调onGetLoginResult的代码,这很丑陋,不过可以避免,比如可以给前面注册给Lua的login函数增加一个参数callbackFunctionName,以让Lua显式的告诉系统当获取登录结果后的回调函数名称。

再次回到本文一开始所提出的问题,尽管在系统中的异步调用不可避免,但我们希望在Lua中能够有同步机制,即Lua脚本在得到验证结果后才允许被继续执行,如何才能做到这一点呢。

Lua有一个非常棒的coroutine机制,在Lua代码中可以通过协同程序来进行多线程,可以使用coroutine.yield和coroutine.resume来对协同程序进行挂起和恢复。需要达到上面所提出的目标,只需在系统中使用与coroutine.yield和coroutine.resume相对应的Lua C API——lua_yield和lua_resume即可。如下所示,login函数有些小改变:

// C code
int login( lua_State *L )
{
	string user = luaL_checkstring( L, 1 );
	string password = luaL_checkstring( L, 2 );
	sendAuthenticationInfo( user, password );
	return lua_yield( L, 0 );
}
// 注册给Lua
lua_register( L, "login", login );

可以看到,与前面的区别只有login函数的最后一句,调用了lua_yield后再返回,而不是return 0,这样就可以达到阻塞Lua脚本的目的。事实是,lua_yield是一个比较特殊的函数,它只能作为注册的C函数的返回值使用,否则调用失败。

当系统获得服务器端的登陆验证结果后,通过lua_resume即可恢复之前被阻塞的Lua。

// C code
// 当服务器端返回认证结果后
BOOL loginResult = ...
lua_pushboolean( L, loginResult );
lua_resume( L, 1 );

上面的代码是当你的系统中只有一个Lua虚拟机的情形,如果使用了多个Lua虚拟机,事情稍微有一点点复杂,login函数还需要将当前调用的lua_State存储下来,以便lua_resume的时候,可以知道恢复的是哪一个被阻塞的虚拟机。

OK,当C中有这样的实现后,Lua程序人员将会为此而高兴,因为Lua代码变得如此简洁。

-- Lua code
-- 当用户点击“登陆”按钮后,执行该函数
function onClickLoginBtn()
	local user = ...
	local password = ...
	if login( user, password ) == true then
		print( "Authentication succeeded." )
		...omitted code...
	else
		print( "Authentication failed." )
		...omitted code...
	end
end

系统or平台开发人员应当尽可能的为用户考虑,正如上面第二种解决方法所追求的那样。

Posted in: C++, Game Programming, Lua / Tagged: C++, coroutine, login, lua, resume, yield, 同步调用

关于从DLL导出模板函数

November 1, 2010 12:10 am / Leave a Comment / Benny Chen

模板在C++中本来就是个比较复杂的领域,而当它和DLL结合到一起时,事情变得更加有点复杂而有趣,最近就遇到了这样一个问题。

我有一个用于生成单件的类模板,它声明在一个DLL项目中,如下。

#if defined MY_EXPORTS
#define EXPORT_API __declspec(dllexport)
#else
#define EXPORT_API __declspec(dllimport)
#endif

template < typename T >
class Singleton
{
public:
	EXPORT_API static T& GetSingleton()
	{
		static T singleton;
		return singleton;
	}

// ...省去其它代码,比如隐藏构造函数,禁止复制构造和赋值函数..
};

我试图导出GetSingleton()函数,因为我希望在其他导入该DLL的模块中能够通过调用这个函数而得到单例。基于该Singleton类模板定义一个单件的类很简单,只需要继承实例化的类即可,比如我在DLL内部定义了两个单件类,LogManager和ResourceManager,他们的定义看起来像这样。

class LogManager : public Singleton< LogManager >
{
// ......
}
class ResourceManager : public Singleton< ResourceManager >
{
// 该类因为需要记录日志,会使用LogManager类
// ......
}

OK,DLL编译通过。然后,我在另一个项目模块中导入该DLL,我需要在这个模块中使用LogManager类和ResourceManager类,由于它们都是单件,正如前面所说的,我通过GetSingleton()函数得到单例,以访问它们的其它接口。

LogManager::GetSingleton()->...
ResourceManager::GetSingleton()->...

然而,状况出现了,build该模块时出现了链接错误:unresolved external symbol “public: static class ResourceManager & __cdecl Singleton::GetSingleton(void)”” ,提示死活链接不到ResourceManager的GetSingleton()函数,然而同样是使用Singleton模板,LogManager的GetSingelton()函数却是链接成功的。

如果足够了解template的机制,就很容易找到问题所在。问题的原因是,除非你显示的实例化(或者调用)了某个模板函数,否则这个模板函数在编译生成代码中(.obj)是根本不存在的。所以,尽管我们为模板函数打上了export的标签,但其实什么也没有导出。而LogManager能链接成功,是因为它在DLL中(ResourceManager中)有使用,从而编译器生成了LogManager::GetSingleton()并导出。

所以,为了能够同样也导出ResourceManager::GetSingleton(),我们必须在DLL中提前显示的实例化它(explicit instantiation)。

LogManager和ResourceManager都是在DLL内部定义的单件类,那能不能在DLL外部也基于Singleton模板快速定义单件类呢?可以,不过GetSingleton这个函数对于其它模块就是隐形的了(根本不存在),你只能在每一个单件类中重新定义一个GetSingleton函数,以避免链接错误。

这个问题有没有完美的解决方案呢,即我们能否让一个模板函数完全导出,使得在别的模块中也可以自由的基于任意类型实例化函数呢?

去google上试图寻求答案,无果。不过说这个问题还真不少,比如CodeProject上的这篇文章:http://www.codeproject.com/KB/cpp/MemberTemplateDLL.aspx 。它提出可以使用inline的方式来解决这个问题,但不保证结果,因为本来编译器对于inline的处理就是个未知的行为。他同时还提出,这可能是一个跟编译器相关的问题,因为对于模板的处理是个跟编译器密切相关的行为。该作者在VC6.0上遇到了这个问题,我的是在VS2005(VC8.0)上,在其他不同平台或版本的编译器上会怎样我也没有测试过。

总之,记住很重要的一点,当导出一个模板函数时,需要格外的警惕。在MSVC编译器上,只有在DLL中被显示的实例化的版本,相应的函数才会被导出。

PS:写于Costa Coffee@中关村,卡布奇诺喝得有点儿恶心了:-)

Posted in: C++ / Tagged: DLL, Singleton, 模板函数

小心DLL链接静态库时的内存错误

September 2, 2010 10:05 pm / 2 Comments / Benny Chen

最近写的模块,在独立的应用程序中测试是没问题的,但把它装配成DLL后,再在另一个应用程序中调用时却出现了内存错误。程序的模块链接关系大概是这样的:

module就是我所写的模块,在这里被封装为DLL,因为要使用json相关的功能,该DLL链接了一个静态库 (jsoncpp.lib)。最后在应用程序中导入并使用module.dll,同时因为在应用程序中也需要用到json,所以应用程序也链接了jsoncpp.lib。

以下用一些伪代码来描述这些模块间的调用关系,以具现出这个错误。

jsoncpp.lib为c++提供了功能齐全的json操作,其核心的类是Json::Value。(阅读本篇文章你无需了解太多json)

module.dll中导出了一个接口:

//ModuleClass.h
#include "json/value.h"

#if defined MODULE_EXPORTS
#define MODULE_EXPORTS __declspec(dllexport)
#else
#define MODULE_EXPORTS __declspec(dllimport)
#endif

class ModuleClass
{
public:
	MODULE_EXPORTS void AllocSomeMemory( Json::Value &root )
	{
		// 这将申请一些内存,因为会new出一个Json::Value,并append到root上
		root.append( "testString" );
	}
};

应用程序:

#include "json/value.h"
#include "ModuleClass.h"
int main()
{
	Json::Value root;
	ModuleClass::AllocSomeMemory( root );
}

在Debug模式下,当main函数执行完毕,对Json::Value root进行析构时,程序便出现了异常。分析下,很显然,调用ModuleClass::MallocMemoryHere时申请的内存,是在module.dll中申请的,而对这些内存的析构则是在应用程序(.exe)中进行的(析构root会同时析构append在root上的所有子Json::Value)。不过,这是异常的真正原因么?

追踪到异常的出错点:dbgheap.c文件中那句ASSERT语句。

/*
* If this ASSERT fails, a bad pointer has been passed in. It may be
* totally bogus, or it may have been allocated from another heap.
* The pointer MUST come from the 'local' heap.
*/
_ASSERTE(_CrtIsValidHeapPointer(pUserData));

注释中的最后一句话”The pointer MUST come from the ‘local’ heap“引起了我的警惕,难道对于内存的申请和释放不是在同一个heap上,除了‘local’ heap还有一个什么heap么。

去MSDN上搜索了关于_CrtIsValidHeapPointer,似乎找到了答案,以下这段话是MSDN上对于_CrtIsValidHeapPointer的介绍:

The _CrtIsValidHeapPointer function is used to ensure that a specific memory address is within the local heap. The local heap refers to the heap created and managed by a particular instance of the C run-time library. If a dynamic-link library (DLL) contains a static link to the run-time library, it has its own instance of the run-time heap, and therefore its own heap, independent of the application’s local heap. When _DEBUG is not defined, calls to _CrtIsValidHeapPointer are removed during preprocessing.

注意字体加粗的部分,这不正应对我的情形么?!错误不在于DLL中申请的内存在EXE中释放,而在于如果这个DLL拥有一个静态链接,它就会拥有独立的运行时堆,独立于应用程序的堆。这样对于内存申请和释放并不是在同一个堆上进行的,当然出错了。

解决:虽然MSDN上最后说,如果把项目改成release的,这个ASSERT就将避免,但这是放纵内存泄露,最好的解决办法是将静态链接也改成动态链接,这样就使得DLL能够和应用程序共享同一个堆,错误也得以避免。

于是,我修改了jsoncpp的项目配置,生成jsoncpp的动态链接库,而不是使用静态库,重新导入到module.dll中,错误解决。

Posted in: C++, Some Experiences / Tagged: _CrtIsValidHeapPointer, DLL, 静态库

Static Initialization Order Fiasco

August 19, 2010 12:59 pm / Leave a Comment / Benny Chen

Static Initialization Order Fiasco (SIOF),我也是最近才知道了这个说法,因为在开发程序的时候被它bug了:对于一个static变量,不管它是全局的或者是类的成员变量,访问它的时候不一定总是成功的,甚至会造成程序crash,因为不能保证它在被访问时已经被初始化了(跟初始化的顺序有关,所以称为初始化顺序的Fiasco)。以下将制造一个非常简单的SIOF情形:

Whatever.h

#include <vector>
#include <string>
class Whatever
{
public:
	Whatever()
	{
		cout << "Construct Whatever" << endl;
		Display();
	}
	~Whatever()
	{
		cout << "Destruct Whatever" << endl;
		Display();
	}
	void Display()
	{
		cout << "static int:" << i << endl;
		cout << "static string:" << m_str << endl;
		cout << "static vector:" << m_vec.front() << endl;
	}

private:
	static int i;
	static std::string m_str;
	static std::vector<char> m_vec;
};

Whatever.cpp

#include "Whatever.h"

int Whatever::i = 500;
string Whatever::m_str = "something";
vector<char> Whatever::m_vec = vector<char>( 10, 'a' );

一个简单的类,Whatever,包含几个static成员变量,然后在构造函数和析构函数中都分别打印这些静态变量的值,乍一看似乎没什么问题,但却有潜在的SIOF的风险。我们容易默认为在调用Whatever的构造函数的时候,Whatever空间中的static的成员变量已经被初始化了,其实不然,现在制造一个SIOF引起crash的情形:

#include "Whatever.h"
Whatever g_whatever; 
int main()
{
	...
}

因为g_whatever是global变量,所以最先被初始化,在调用Whatever的构造函数的时候,Whatever空间的静态成员变量还未被初始化,所以访问这些静态变量肯定出错。在VS的编译器下测试的结果:

Construct Whatever
static int:5
static string:
(调用m_vec.front()导致程序crash)

奇怪的是对于int这种built-in的类型却能得到正确的值,不知编译器在背后都做了哪些手脚,猜想可能是在程序编译的时候他们就被值替换了。而string和vector应该都属于自定义类型(初始化需要调用构造函数),未初始化之前访问肯定是错误的,所以打印出的string是个空值,而访问一个空的vector的front元素则直接造成程序crash。

根据初始化和释放的对称关系,所以在析构函数中访问这些静态变量同样也是失败的,因为在析构g_whatever的时候,Whatever空间的静态变量已经被解决掉了。

SIOF是非常难于检测的问题,这个例子是一种最简单的情形,在我的项目中,我并没有定义什么global的成员,但是因为使用了很多前置声明(forward declaration),还有一些Singleton,造成了一个非常隐蔽的SIOF,花了很大的力气才找到,痛苦的过程。

要解决SIOF问题,需要用一个function来包装static变量,即利用函数内static变量的construct-on-first-use特性。

修改后的Whatever.h

class Whatever
{
public:
	Whatever()
	{
		cout << "Construct Whatever" << endl;
		Display();
	}
	~Whatever()
	{
		cout << "Destruct Whatever" << endl;
		Display();
	}
	void Display()
	{
		cout << "static vector:" << GetStaticVector().front() << endl;
	}

private:
	vector<char>& GetStaticVector()
	{
		static vector<char> vec = vector<char>( 10, 'a' );
		return vec;
	}
};

用GetStaticVector来包装之前所需要的静态的vector,就能保证在调用的时候,它一定已经被初始化了。再次运行之前的测试程序,OK了。

总之,我们对于static变量的使用要保持一颗警惕的心,如果不确定在使用时它是否已经被初始化,就要使用函数包装static变量来防止Static Initialization Order FIASCO!
Fiasco, what a cool word.

Posted in: C++ / Tagged: crash, static initialization order fiasco, 初始化, 崩溃, 静态变量, 顺序

让管理lua_State的类指针安全

August 15, 2010 4:35 pm / 1 Comment / Benny Chen

最近因为在公司的项目中接手了Lua脚本模块的开发,所以研究了很多关于Lua脚本的东西,秉着“多看多想多记”的原则,我时刻敦促自己要及时记录下遇到的一些问题和想法。

在Lua中,几乎所有的东西都是围绕着lua_State转,所以,一般我们都会写一个类来封装管理它,比如:

class LuaObject
{
public:
	LuaObject()
	{
		m_L = luaL_newstate();
		luaL_openlibs( m_L );
	}
	~LuaObject()
	{
		if ( m_L )
		{
			lua_close( m_L );
			m_L = NULL;
		}
	}

private:
	lua_State *m_L;
};

这很好,不过它不是指针安全的。试想,如果一个LuaObject对象被复制,结果将会怎样。

LuaObject luaObject1;
LuaObject luaObject2( luaObject1 );

上面这段代码将会导致运行时crash,因为luaObject1和luaObject事实上指向了同一块lua_State,这样当luaObject1和luaObject2被析构时,lua_State会被两次lua_close,这不crash才怪呢!

事实是,当一个类包含了一个指针时,我们就需要开始变得格外谨慎,除了在构造函数和析构函数中要处理指针的初始化和清理外,我们还需要考虑深拷贝(deep copy),浅拷贝(shallow copy))的问题。如果使用编译器默认生成的拷贝构造函数,它只会浅拷贝指针,而指针所指向的内存区域不会被拷贝。就像上面一样,两个LuaObject实则共享了一个lua_State。

那该如何处理让管理LuaObject类的指针安全呢,深拷贝?厄,首先我也没有深究深度拷贝lua_State具体该如何完成,不过我猜想这可是一个复杂而重型的操作,仅仅为了带来指针安全而选择此可不是一个明智的选择。

如果你是开发LuaObject类的程序员,也许你会对使用LuaObject的程序员(或许有时使用者就是你自己)说:“你不要对它进行拷贝操作不就OK了”,但这是一种严重不负责任的行为,因为这种皮球会越踢越远的。比如,程序员A使用了你的LuaObject类,他写了一个包含LuaObject的指针的类,同样A也不考虑拷贝指针安全问题,然后A又将它的类传递给了B,B封装了A的类指针,然后传递给了C,接着如此重复,C再给D,D再给E,最后E在对类进行拷贝操作时,程序crash掉了,因为在某个最底层,他们共享了lua_State。不过因为这时候已经嵌套了这么多层,E程序员或许根本不知道也不关心什么是LuaObject(他只跟D的类打交道)。这时,要想追踪到BUG之源——万恶的不考虑指针安全的LuaObject,已经非常困难了,God bless you all.

事实是,如果你不想让你的类被复制,你就应该明确而显示的禁止它。其实lua_State的这个问题非常类似于Effective C++书中的第14个条款中所提到的问题:在资源管理类中小心copying行为。对于这个问题,书中提供了两种解决方案。第一种就是禁止复制。第二种则是对底层资源的“引用计数法”(reference count)。

第一种禁止复制很简单。(关于Uncopyable)

class LuaObject : private Uncopyable
{
public:
    ...
}

第二种则是使用智能指针来封装lua_State,我们可以使用boost的shared_ptr来封装lua_State,即shared_ptr< lua_State > m_L。如何对m_L进行初始化是另一个需要注意的问题,如果使用shared_ptr的常见初始化形式:m_L = shared_ptr< lua_State >( luaL_newstate() ),这样是不对的,因为在这种形式下,当lua_State的计数变为0时,shared_ptr会去调用lua_State的的析构函数,这显然是错误的,对lua_State的释放动作是lua_close而不是删除。事实上,这样编译器也无法通过,如果这么写,会报出“use of undefined type ‘lua_State’”的错误,提示lua_State是一个非完成的类型(incomplete type)。

我们应该为shared_ptr的初始化传入一个删除器(deleter)。很显然,lua_State的deleter是lua_close()函数,这样最终的代码如下。

#include <boost/shared_ptr.hpp>
using boost::shared_ptr;

class LuaObject
{
public:
	LuaObject()
	{
		m_L = shared_ptr< lua_State >( luaL_newstate(), lua_close );
		luaL_openlibs( m_L.get() );
	}

private:
	shared_ptr< lua_State > m_L;
};

这样再回到前面拷贝的那个例子,luaObject1和luaObject2共同引用了一个lua_State,但因为使用了shared_ptr,所以只有在lua_State的引用次数变为0时,它的deleter(这里是lua_close)才会被调用,安全了!

在我的项目中,因为没有使用boost库,也没有提供任何智能指针,所以使用禁止复制来保证安全。

Posted in: C++, Game Programming / Tagged: boost, copy, lua, lua_State, pointer, safe, shared_ptr, 安全, 指针

Faint!

March 29, 2010 10:08 pm / 1 Comment / Benny Chen

以后干什么事热血冲动之前,还是最好先做一下survey。前几天异常兴奋的开始翻译Google的C++风格指南,结果今天到Google上简单一搜搜出了一堆,这神圣的事情已经被无数人干过了。

Faint!

我也只是才翻译了一个chapter,但已经没有必要再继续下去了。

Google 开源项目风格指南 – 完整中文版在这里:http://code.google.com/p/zh-google-styleguide

Posted in: C++

[译]Google的C++风格参考指南(1)– 头文件

March 28, 2010 8:15 pm / Leave a Comment / Benny Chen

译者的话:

在一个开发团队中,很重要的一点是要遵循一个统一的风格,这样才能便于相互之间在代码上进行快速的交流和理解。反之,如果团队中的每个人只是按照自己的个性去写属于个人风格的代码,这必然会造成项目代码的五花八门,互相之间难以沟通,整个项目的代码质量低下,BUG率攀高,软件的可维护率也极差。长此以往,国将不国,团队将不团队,最终软件极有可能以一种极其混乱的状态收尾,这真是一个悲剧。

每一个C++程序员都有自己的编程风格,虽然大家的风格迥异,但也一定有一些共通的地方。我希望能找到一个对于C++风格的归纳和总结,这样在以后我的工作中,对我自己,甚至对我工作的团队,当然也对于正在阅读的你,能够有一个帮助和参考。所以,我决定翻译Google的这篇C++风格参考指南。当然,真正促使我去翻译这篇文章的原因是,我是一个C++技术和简洁代码风格的狂热追捧者。

注意,这只是一篇参考,它是Google的C++风格,你并不一定严格的遵循所有的规则,永远记住很重要的一点,风格永远跟着你的团队走。

本人能力有限,所以不一定每个地方都翻译的准确,另外一些不太好翻译的专业名词首次出现时,我会标注英文原文。

原文:Google C++ Style Guide

背景 (Background)

C++是很多Google开源项目所使用的开发语言。正如每一个C++程序员所熟知的,C++有很多强大的功 能,但功能强大也同时意味着复杂性(complexity),使得代码bug率更高且难以阅读和维护。

这篇参考指南 (guide)的目的在于,通过详细描述应该做的(dos)和不应该做的(don’ts),来控制我们撰写C++代码时的复杂性。这些规则令程序员在高效利用C++语言特性的同时,还能让手工写出的所有代码(code base)易于管理。

风格(style),或者可 读性(readability),我们把它称作为“用来管理我们C++代码的常用惯例(convention)”。不过用“风格”这个术语有些不当,因为这些惯例并不只是指代码源文件的格式。

让代码可管理,一种方式是加强代码的一致性(consistency)。令任何一个程序员能够快速的理解另一个程序员所写的代码是非常重要的。如果代码保持统一的风格并且遵循惯例,我们可以更加轻松的使用“模式匹配”(pattern- matching)推断出,不同的符号分别代表什么。创造这种代码公共性(common)需要习语(idiom)和模式(pattern),使得代码易于理解。在某些情况下,也许有好的理由来改变某些风格规定,但为了获得一致性,不管怎样我们还是得保持风格。

这篇参考指南所关注的另一点是C++的功能膨胀问题。C++是一个有很多高级特性的庞大语言。在某些情况下,我们限制甚至禁止使用一些特性。这样做是为了保持代码的简洁,并避免因使用这些特性而带来各种常见的错误和问题。指南里会列出这些特性,并解释为何限制使用它们。

Google 所开发的开源项目遵守这篇参考指南里所列出的规定。

注意,这篇指南不是C++教程,我们认为所有正在阅读的同志们都是熟悉C++的。

头文件(Header Files)

一般而言,每个.cc文件都有一个关联.h文件。但也有一些例外,比如用于单元测试(unit test)的.cc文件或者只包括一个main()函数的小型.cc文件。

正确的使用头文件可以让代码的可读性、规模(scale)和性能(performance)得到很大的改观。

接下来介绍的一些规则将会引导你越过使用头文件时的各种陷阱。

#define防护(The #define Guard)

所有的头文件都应该使用#define防护来防止多重包含(multiple inclusion),格式是<PROJECT>_<PATH>_<FILE>_H_

为了保证唯一性(uniqueness),该格式应该基于源文件在整个项目中的路径。比如说,项目foo中的头文件foo/src/bar/baz.h应该用如下的格式:
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_

…

#endif // FOO_BAR_BAZ_H_

头文件依赖(Header File Dependencies)

如果使用前置声明(forward declaration)就可以解决依赖,就不要用#include。

当你#include一个头文件的时候,你同时也引入了依赖关系————每当头文件有任何的修改,包含该头文件的代码也要重新编译。如果你的头文件还包含了其他的头文件,那任何对于这些文件的修改会导致任何包含了你的头文件的代码重新编译。因此,我们倾向于尽量减少使用#include,尤其避免在一个头文件中包含另一个头文件。

使用前置声明,可以显著的减少在你的头文件中#include其他头文件的次数。比如说,如果你的头文件要用到class File,但你不需要访问class File的详细定义,那你的头文件只需前置声明class File即可,而不是#include “file/base/file.h”。

那在哪些情况下,我们是只使用一个类(比如 class Foo),而无需访问它的定义呢?

  • 声明类数据成员类型为 Foo* 或者 Foo&
  • 声明(但不定义)参数或返回值为Foo的函数
  • 声明静态数据成员类型为Foo。这是因为静态数据成员在class之外定义

另 一方面,如果你的类继承自Foo或者你有一个类数据成员是Foo,那就必须包含Foo的头文件。

有些时候,使用指针 (scoped_ptr 更佳)成员而不是对象成员(object member)是明智的。然而这却降低了代码的可读性,并且导致性能损失。所以,如果这么做的目的只是减少头文件中的#include,那就别这么做。

当 然,.cc文件必须需要它所使用的类的定义,所以它会#include比较多的头文件。

内联函数(Inline Functions)

只当函数很小,比如只有10行或更少时,才定义它为内联函数。

解释:你可以以这种方式定义函数,该方式允许编译器将函数内联展开,而不是以传统的函数调用机制去调用它们。

优点:内联函数可以产生更加高效的目标代码(object code),但前提是内联函数要足够小。你可以随意的将这些函数设置为内联函数:访问函数(accessor),设置函数(mutator),以及其他的短小的并与性能紧密相关的函数。

缺点:过度使用内联函数会导致程序变慢。内联一个函数时,根据该函数的大小,内联会增加或降低代码的规模。内联一个很小的访问函数通常会降低代码规模,而内联一个非常庞大的函数则会大大增加代码的规模。在现代的处理器 (processor)上,因为使用指令缓存(instruction cache)的缘故,短小的代码通常跑的更快。

结论:

一个不错的经验法则是,不要内联超过10行的函数。要小心析构函数,它们通常要比表面上看到的要长很多,因为它们会隐式的调用成员或者基类的析构函数。

另一个有用的经验是,内联带有循环或者switch语句的函数是低效的(除非循环或switch语句从未被执行过)。

还有重要的一点是,就算函数被声明为内联,但它不一定就被内联。比如说,虚函数或者递归函数通常都不被内联。一般我们也不应该声明递归函数为内联函数。而通常我们把一个虚函数声明为内联函数的主要原因是,将它的定义放在类定义内,这样是为了图方便或者将它文档化(document)的行为,比如说在访问函数和设置函数上我们就是这么干的。

-inl.h文件(The -inl.h Files)

当你需要使用复杂的内联函数时,你可以使用文件名 后缀为-inl.h的文件来定义它。

内联函数的定义要放在头文件里,这样编译器在函数调用点才能访问到定义用于内联。然而,函数的实现代码通常放在.cc中,而且我 们也不喜欢把太多的实际代码放在.h文件中,除非这么做有利于可读性或性能。

如果一个内联函数的定义很短, 里面几乎没有任何逻辑,你应该把这样的定义代码放到.h文件中。比如说,访问函数和设置函数应毫无疑问地放在类定义中。稍复杂一些的内联函数定义也可能放 在.h文件中,这样能方便实现者(implementer)和调用者(caller)。尽管这样做可能使得.h文件有些笨重,但你可以把代码放到另一个 -inl.h文件中。这将实现(implementation)从类定义中分离,而且使得实现能够在需要的地方被包含。

-inl.h 文件还可以用于函数模板的定义,这使得你的模板定义易于阅读。

当然不要忘了,像其他的头文件一样,-inl.h文件 也需要#define防护。

函数参数顺序(Function Parameter Ordering)

当定义一个函数时,参数的顺序应该是:先输入参数,再输出参数。

C/C++函数的参数可以是函数的输入,函数输出,或者两者兼有。输入参数通常是值(values)或者常量引用(const reference),而输出参数和输入/输出参数是非常量指针(non-const pointer)。对于函数参数的排序,将纯输入参数放在任何输出参数之前。尤其不要因为一个参数是新添加的,就把它放在函数参数的最后,要将纯输入参数放在输出参数之前。

这不是个硬性规则。那种既是输入又是输出的参数(通常是class或者struct)会让情况变 得更复杂,还有一点,就像前面所说的一样,为了与相关函数保持一致性,也会需要违背这一规则。

文件包含的名字和顺序(Names and Order of Includes)

请使用以下标准顺序以保证可读性,并且避免隐藏的依赖:C库,C++库,其他库的.h,你的项目的.h。

所有项目的头文件应作为项目源文件目录的后代列出,不要使用UNIX中的目录快捷方式 . (当前目录)和 .. (父目录)。

举个例子,google-awesome-project/src/base/logging.h应该以#include “base/logging.h”的格式包含。

在 dir/foo.cc中,该文件的主要目的是实现或者测试在dir2/foo2.h中的东西,文件包含应按照如下排序:

  1. dir2/foo2.h (首选位置(preferred location) — 请见下面的详细说明).
  2. C system files.
  3. C++ system files.
  4. Other libraries’ .h files.
  5. Your project’s .h files.

首选的排序(preferred ordering,译者注:在以上例子中为dir2/foo2.h )降低了隐藏的依赖。我们希望每个头文件能够自我编译。最简单的方式就是确保它们中的每一个都在某个.cc文件中是第一个被#include的。

dir/foo.cc and dir2/foo2.h通常在同一目录下(比如base/basictypes_unittest.cc and base/basictypes.h),但也可能在不同的目录下。

在每个部分(section)中,根据字母顺序排列是不错的方式。

举个例子,google-awesome-project/src/foo/internal/fooserver.cc中的文件包含顺 序可能是这样:

#include “foo/public/fooserver.h” // 首选位置

#include <sys/types.h>
#include <unistd.h>

#include <hash_map>
#include <vector>

#include “base/basictypes.h”
#include “base/commandlineflags.h”
#include “foo/public/bar.h”

未完待续…

Posted in: C++ / Tagged: C++, Google, 参考指南, 头文件, 风格

日积月累: Uncopyable & Uninheritable in C++

November 13, 2009 4:45 pm / 7 Comments / Benny Chen

如何在C++中阻止复制和继承呢,我做了个整理。

Uncopyable
有些类需要禁止被复制或者赋值,这需要显示的禁掉它们的拷贝构造和赋值函数。
摘自《Effective C++》Item 6: Explicitly disallow the use of compiler-generated functions you do not want

class Uncopyable 
{
protected:                                   // allow construction
    Uncopyable() {}                       // and destruction of
    ~Uncopyable() {}                     // derived objects...

private:
    Uncopyable(const Uncopyable&);             // ...but prevent copying
    Uncopyable& operator=(const Uncopyable&);
};

class UncopyableExample: private Uncopyable 
{ 
    // THIS CLASS IS UNCOPYABLE
}; 

Uninheritable
Java提供了final关键字用来阻止被继承,而在C++中没有这个关键字,但阻止继承的类在C++中是同样可能的。
摘自:http://www.csse.monash.edu.au/~damian/Idioms/Topics/04.SB.NoInherit/html/text.html

template <class OnlyFriend>
class Uninheritable
{
    friend class OnlyFriend;
    Uninheritable(void) {}
};

class NotABase : virtual public Uninheritable< NotABase >
{
    // WHATEVER
};

class NotADerived: public NotABase
{
    // THIS CLASS GENERATES A COMPILER ERROR
    NotADerived(){}
};
Posted in: C++, Talking in Code / Tagged: C++, 禁止复制, 禁止继承

LinkedIn

Milan Petrovic

Categories

  • In My Life (25)
    • A Day in the Life (8)
    • English Learning (2)
    • Learn a Word (7)
    • Something In The Way (8)
  • Music Heaven (8)
    • Guitar (1)
    • In Concert (1)
    • Lyrics (3)
  • OK Computer (54)
    • 3D (3)
    • C++ (10)
    • Computer Graphics (15)
    • Game Programming (23)
    • iOS (6)
    • Linux (1)
    • Lua (9)
    • My Projects (3)
    • Some Experiences (9)
    • Talking in Code (2)
    • Unity (2)
  • Quotations (2)
  • Uncategorized (1)
  • Visca Barça (24)
    • FCB BJ (5)

Recent Posts

  • [译]优化你的手机游戏(没有延迟的,才是健康的)- 一篇给游戏美术设计师读的文章
  • 新浪微博API for MOAI
  • 稍后继续
  • Unity Developer ++
  • Another Thread @ Moai Forum
  • 1st Day of Golden Week
  • 为SyntaxHighlighter添加新语言
  • 基于Lua的State Pattern
  • Class Diagram of Pacman
  • 基于Moai的Pacman

Recent Comments

  • 约修亚_RK on 为SyntaxHighlighter添加新语言
  • 爱装的小男孩 on 小心DLL链接静态库时的内存错误
  • happyfire on Game Loop的几种实现方式
  • William on 新浪微博API for MOAI
  • Benny Chen on 新浪微博API for MOAI
  • your man on 新浪微博API for MOAI
  • 你家男人 on 稍后继续
  • 逍遥 on 关于对std::vector的遍历
  • papa on Unity Developer ++
  • T客网 ︱ Techpot » Blog Archive » iOS开发与OpenGL ES相关问题整理(1) on iOS开发与OpenGL ES相关问题整理(1)

Tags

2d 3D 3dsmax 3ds max air Apply architecture Asia tour barca Beijing bilbao binary search blocked bob boost bruce springsteen C++ capo CGContextDrawImage Champions League Change DLL DX10 eval exporter flash framework frustum culling game game engine iniesta ios linux lua Moai opengles pacman plug-in plugin 北京 导出插件 崩溃 巴萨 游戏引擎 踢球
© Copyright 2026 - A Game Developer
Infinity Theme by DesignCoral / WordPress