关于从DLL导出模板函数

模板在C++中本来就是个比较复杂的领域,而当它和DLL结合到一起时,事情变得更加有点复杂而有趣,最近就遇到了这样一个问题。 我有一个用于生成单件的类模板,它声明在一个DLL项目中,如下。 我试图导出GetSingleton()函数,因为我希望在其他导入该DLL的模块中能够通过调用这个函数而得到单例。基于该Singleton类模板定义一个单件的类很简单,只需要继承实例化的类即可,比如我在DLL内部定义了两个单件类,LogManager和ResourceManager,他们的定义看起来像这样。 OK,DLL编译通过。然后,我在另一个项目模块中导入该DLL,我需要在这个模块中使用LogManager类和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中被显示的实例化的版本,相应的函数才会被导出。

Posted in: C++ by Benny Chen No Comments

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

最近写的模块,在独立的应用程序中测试是没问题的,但把它装配成DLL后,再在另一个应用程序中调用时却出现了内存错误。程序的模块链接关系大概是这样的: module就是我所写的模块,在这里被封装为DLL,因为要使用json相关的功能,该DLL链接了一个静态库 (jsoncpp.lib)。最后在应用程序中导入并使用module.dll,同时因为在应用程序中也需要用到json,所以应用程序也链接了jsoncpp.lib。 以下用一些伪代码来描述这些模块间的调用关系,以具现出这个错误。 jsoncpp.lib为c++提供了功能齐全的json操作,其核心的类是Json::Value。(阅读本篇文章你无需了解太多json) module.dll中导出了一个接口: 应用程序: 在Debug模式下,当main函数执行完毕,对Json::Value root进行析构时,程序便出现了异常。分析下,很显然,调用ModuleClass::MallocMemoryHere时申请的内存,是在module.dll中申请的,而对这些内存的析构则是在应用程序(.exe)中进行的(析构root会同时析构append在root上的所有子Json::Value)。不过,这是异常的真正原因么? 追踪到异常的出错点:dbgheap.c文件中那句ASSERT语句。 注释中的最后一句话”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 […]

Posted in: C++ by Benny Chen No Comments , ,

Static Initialization Order Fiasco

Static Initialization Order Fiasco (SIOF),我也是最近才知道了这个说法,因为在开发程序的时候被它bug了:对于一个static变量,不管它是全局的或者是类的成员变量,访问它的时候不一定总是成功的,甚至会造成程序crash,因为不能保证它在被访问时已经被初始化了(跟初始化的顺序有关,所以称为初始化顺序的Fiasco)。以下将制造一个非常简单的SIOF情形: Whatever.h Whatever.cpp 一个简单的类,Whatever,包含几个static成员变量,然后在构造函数和析构函数中都分别打印这些静态变量的值,乍一看似乎没什么问题,但却有潜在的SIOF的风险。我们容易默认为在调用Whatever的构造函数的时候,Whatever空间中的static的成员变量已经被初始化了,其实不然,现在制造一个SIOF引起crash的情形: 因为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 用GetStaticVector来包装之前所需要的静态的vector,就能保证在调用的时候,它一定已经被初始化了。再次运行之前的测试程序,OK了。 总之,我们对于static变量的使用要保持一颗警惕的心,如果不确定在使用时它是否已经被初始化,就要使用函数包装static变量来防止Static Initialization Order FIASCO! Fiasco, what a cool word.

让管理lua_State的类指针安全

最近因为在公司的项目中接手了Lua脚本模块的开发,所以研究了很多关于Lua脚本的东西,秉着“多看多想多记”的原则,我时刻敦促自己要及时记录下遇到的一些问题和想法。 在Lua中,几乎所有的东西都是围绕着lua_State转,所以,一般我们都会写一个类来封装管理它,比如: 这很好,不过它不是指针安全的。试想,如果一个LuaObject对象被复制,结果将会怎样。 上面这段代码将会导致运行时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) 第二种则是使用智能指针来封装lua_State,我们可以使用boost的shared_ptr来封装lua_State,即shared_ptr m_L。如何对m_L进行初始化是另一个需要注意的问题,如果使用shared_ptr的常见初始化形式:m_L = shared_ptr( 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()函数,这样最终的代码如下。 这样再回到前面拷贝的那个例子,luaObject1和luaObject2共同引用了一个lua_State,但因为使用了shared_ptr,所以只有在lua_State的引用次数变为0时,它的deleter(这里是lua_close)才会被调用,安全了! 在我的项目中,因为没有使用boost库,也没有提供任何智能指针,所以使用禁止复制来保证安全。

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

译者的话: 在一个开发团队中,很重要的一点是要遵循一个统一的风格,这样才能便于相互之间在代码上进行快速的交流和理解。反之,如果团队中的每个人只是按照自 己的个性去写属于个人风格的代码,这必然会造成项目代码的五花八门,互相之间难以沟通,整个项目的代码质量低下,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),格式是 1<PROJECT>_<PATH>_<FILE>_H_ 为了保证唯一性(uniqueness),该格式应该基于源文件在整个项目中的路径。比如说,项目foo中的头文件foo/src/bar/baz.h应该用如下的格式: 123456#ifndef  FOO_BAR_BAZ_H_ #define FOO_BAR_BAZ_H_ … #endif //  FOO_BAR_BAZ_H_ 头文件依赖(Header […]