不要在公共接口中传递STL容器

最近的一个项目,是开发一个framework,提供给公司内部不同的产品线使用。 之间遇到的一个问题,就是STL容器的使用, 而结论是不要在公共接口中传递STL容器:

  • 这里说的STL容器,但主要则是指容器,字符串类,但其实可以推广到在STL中提供的任何类型,
  • 这里说的公共接口,是指需要暴露给客户的sdk头文件,包括函数签名,或者类成员变量;

也可以说,不要在暴露给客户的头文件中包含STL的头文件。

原因分析

为什么有这个结论,我们可以从几个方面来论述:

  • 客户端使用的STL版本可能不同
    因为STL作为标准库,Framework编译的时候使用的STL,与客户端编译的时候使用的STL,版本是有可能不一样的,比如Framework使用VC8编译发布,而客户代码使用VC10编译使用,那么Framework所理解的STL容器,与客户端代码所理解的STL容器,在内存布局上,数据表示上是有可能不一样的,出错也就不可避免了。
  • 编译选项
    即使Framework和客户端使用的是同一个版本的STL,但是如果编译选项不一样,也可能导致其不一致性,自然也会出错。
  • 静态变量
    即使版本一致,编译选项一致,仍然是不安全的,因为STL容器的实现中,有可能使用了静态变量,而在Framework模块和客户端模块中,虽然由于模板扩展产生的两份代码总体上是一致的,但都各自维护了一份静态变量,从而造成了内部状态的不一致性。
  • 客户端的STL被自定义
    但从文件的角度来看,在客户端包含的STL头文件实现,可以是完全不同于标准的 - 只要其接口和标准保持一致。 比如客户那边使用的STL在内存管理方面有特殊需求,改写了默认的allocator,那么内存必然会出问题;比如客户在实现中加入了其他成员变量,那么内存布局必然也不一致。

虽然,微软这篇文章提到导出vector是安全的,但这应该是有风险的:

除非能够确信你的DLL和DLL的使用者会使用完全相同的STL - 版本一致,编译选项一致,没有被自定义过等,不然就可能导致行为的不一致性:
举个例子,你在DLL中导出了vector<int>,而这个DLL的客户使用的stl是自定义的版本,内存管理由其自己实现,那么vector<int>在客户方就可能有了两种含义。

可选方案

但事实上,你还是可能存在这种需求的 - 你需要容器类的函数参数或者返回值,你可能也需要容器类的类成员变量,那么如何解决? 

对于前者,我们可以针对具体的类型封装一个具体的容器类,内部还是可以使用STL container的;
对于后者,我们可以用一些办法,防止STL容器出现在头文件中; 

解决的方法,可以有以下几种:

  • 提供自己的容器类
    这是最彻底,最直接的方法,但是提供一套与STL相当的模板容器类需要蛮大的工作量,而且明显是重复制造轮子了。 要注意,这里简单的对STL容器进行封装是行不通,因为模板不支持分离编译,这么做还是会把STL容器带入公共接口。
  • PImpl
    PImpl模式当然很强大,当然可以解决这个问题,但是此处如果仅仅是为了隐藏容器类,难免杀鸡用牛刀,还引入了不必要的间接层与繁琐的脚手架代码。
  • 使用void*作为数据成员,而后强制转换为容器类
    不安全也太丑陋,用起来也繁琐,每次都要cast以下,除非别无他法,是在不应该考虑这个方案。 
  • 前置声明STL容器 
    这个方法在VC9中是可以工作的,但是根据C++标准: 17.4.3.1 Reserved names,这种做法是未定义的:
    “ It is undefined for a C++ program to add declarations or definitions to namespace std or namespaces within namespace std unless otherwise specified”

  • 自定义结构体封装容器并前置声明
    这个方案是这样的:
    //头文件
    struct PublicClassData;
    class PublicClass
    {
    public:
    PublicClass();

    private:
    PublicClassData
    * m_pData;

    }

    //实现文件
    struct PublicClassData
    {
    vector
    <int> _array;
    map
    <string, int> _map;
    };

    PublicClass::PublicClass()
    {
    m_pData
    = new PublicClassData;
    }

    当然,需要注意的是我们需要遵循:rule of three,提供析构,赋值与拷贝构造函数以管理内存。
    这个方案相对来讲比较简洁,应该说是现有方案的最有实践性的一个。

结论

所以,结论就是不要在DLL/SO的公共接口中使用STL容器,如果你确实需要,那么请用自定义结构体封装容器并前置声明的方式隔离STL容器!

posted @ 2011-07-10 19:32 lzprgmr 阅读(2492) 评论(11) 编辑 收藏

 回复 引用 查看   
#1楼 2011-07-11 09:23 陈硕      
“静态变量”这个理由对 DLL 或许是正确的,对 SO 则是错误的。
能否在动态库的界面上使用STL,主要取决于贵公司 C++ 开发环境的完整性(integrity)。

 回复 引用 查看   
#2楼[楼主] 2011-07-12 06:53 lzprgmr      
@陈硕

>>能否在动态库的界面上使用STL,主要取决于贵公司 C++ 开发环境的完整性(integrity)。
由于公司有许多产品线,不同部门,其开发环境升级、是否使用了自定义的STL都是未知的,也是可变的,所以不使用是比较安全的做法。

>>“静态变量”这个理由对 DLL 或许是正确的,对 SO 则是错误的。
对SO了解不是很深,我理解的是两个SO中的容器代码应该是两份独立的代码(由模板产生),那么在SO中他们是怎么做到共享静态变量的?

 回复 引用 查看   
#3楼 2011-07-12 12:05 陈硕      
@lzprgmr
用 weak symbol ,由 linker & loader 去重。
http://www.codesourcery.com/public/cxx-abi/abi.html#vague


"V"
"v" The symbol is a weak object. When a weak defined symbol is linked with
a normal defined symbol, the normal defined symbol is used with no
error. When a weak undefined symbol is linked and the symbol is not
defined, the value of the weak symbol becomes zero with no error. On
some systems, uppercase indicates that a default value has been
specified.

 回复 引用 查看   
#4楼 2011-07-16 12:18 RaymondSQ      
如果确实有这样的担心,可以使用自定义结构体传输,反复折腾STL似乎也比较麻烦。
 回复 引用   
#5楼 2011-07-19 23:28 lzcpp[未注册用户]
别说传个stl容器了,随便一个C++ object也不敢传啊。
 回复 引用 查看   
#6楼[楼主] 2011-07-23 07:13 lzprgmr      
@RaymondSQ
引用RaymondSQ:如果确实有这样的担心,可以使用自定义结构体传输,反复折腾STL似乎也比较麻烦。

但是你又不愿放弃STL提供的那些强大、优雅的功能 - 自己写实在不太好

 回复 引用 查看   
#7楼[楼主] 2011-07-23 07:13 lzprgmr      
@lzcpp
引用lzcpp:别说传个stl容器了,随便一个C++ object也不敢传啊。

为什么?

 回复 引用 查看   
#8楼 2011-07-31 22:54 飘行天下      
不同组件之间传递STL,除了Allocator的不同;
我所碰到的还有不同编译选项带来的烦恼:似乎在VS2008,VS引入_SECURE_SCL,会对STL做一些边界条件的安全检查,我们使用的3rd Party内_SECURE_SCL = 1,而主程序因为某个最重要的库而选择0,记得当时重视莫名其妙Crash,查了好久,才知道是这个东西惹的祸啊。

 回复 引用 查看   
#9楼[楼主] 2011-08-06 20:30 lzprgmr      
@飘行天下
引用飘行天下:
不同组件之间传递STL,除了Allocator的不同;
我所碰到的还有不同编译选项带来的烦恼:似乎在VS2008,VS引入_SECURE_SCL,会对STL做一些边界条件的安全检查,我们使用的3rd Party内_SECURE_SCL = 1,而主程序因为某个最重要的库而选择0,记得当时重视莫名其妙Crash,查了好久,才知道是这个东西惹的祸啊。


是啊,使用了SECURE_SCL的容器,其内部会多出一些数据以便check某个iterator是否valid,内存上明显就不匹配了先

 回复 引用 查看   
#10楼 2011-09-21 17:37 .Jx.      
传递那个结构不需要把结构的接口提供给外界吗?那么外界怎么使用容器中的数据?我觉得这种方案还是有问题啊?
 回复 引用 查看   
#11楼[楼主] 2011-09-21 21:47 lzprgmr      
@.Jx.
引用.Jx.:传递那个结构不需要把结构的接口提供给外界吗?那么外界怎么使用容器中的数据?我觉得这种方案还是有问题啊?


结构体定义在cpp中,头文件里只是结构的前置声明,外界是看不到其具体蒂定义的

发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 2102484 /QcfFQTc6+M=

黄将军