免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
楼主: xiaomiao
打印 上一主题 下一主题

使用class template扩展STL map容器类对象的一个例子 [复制链接]

论坛徽章:
0
11 [报告]
发表于 2006-10-01 11:18 |只看该作者
用shared_ptr

论坛徽章:
0
12 [报告]
发表于 2006-10-01 14:44 |只看该作者
看了各位就此问题的深入讨论,受益非浅!我贴上的代码段来源自国内中兴通讯公司某项目的Source Code片断,本来觉得该公司C++高人很多,但看来有这么多隐含的小问题,请哪位高手写个更好的实现?用Shared_ptr要怎么做?多谢指教!

论坛徽章:
0
13 [报告]
发表于 2006-10-02 00:54 |只看该作者
To wolf0403

经过你的指正,终于认识到我以前的在非虚析构函数类的继承上的结论过于简单化了。我上面说:“千万不要把 C++ 标准库中的容器类作为基类使用”,现在收回这句话,并重新订正自己的看法:

非虚析构函数类(比如 STL 容器类)在下列前提条件或者下列情况下可以作为基类继承使用:
  • 如果能限制用户使用基类指针删除派生类对象。限制的方法或者是在文档中进行说明,或者象 wolf0403 所说的从基类进行 private(或者 protected)继承。
  • 如果派生类的析构函数是 trivial 的。所谓 trivial 析构函数是指非用户声明的析构函数、并且类的非静态数据成员和基类(如果有的话)的析构函数也是 trivial 的,其实就是没有任何实际作用的函数。既然这样,调用与不调用这个析构函数也就没有什么关系了,所以即使通过基类指针释放派生类对象中存在的问题也就不成为问题了。

楼主程序中派生类的析构函数是 non-trivial 的,所以可按照条件 1 来做以避免可能出现的问题;或用其它方案(如上面提到的成员法、或将指针换为 shared_ptr)等。

对于你的下面的这个结论:

>> 简单的结论是,指针容器在所有情况下完胜基于值语意的 STL 容器。

我认为并不正确。还是那句话:如果你的这个结论是正确的,那么由于容器无论是值语义还是引用语义都不影响容器的使用界面和通用性,此时效率就成为了主要的决定因素,STL 容器会毫不犹豫地是你所说的指针容器(或者说是引用语义的容器),而不是(或者不仅是)现在的基于值语义的容器。得出这样片面的结论是因为你只是从单一方面、即只从拷贝对象的消耗方面来考虑问题的,没有根据要解决问题进行综合考虑。

和基于值语义的容器一样,使用引用语义的容器也是有代价的。这个代价主要表现在两方面(上面已经说过了):间接访问方式以及引用计数。这就导致了:
  • 向容器中插入对象的时候:取对象地址存入容器,更新引用计数
  • 通过迭代器引用对象的时候:直接返回对象地址,然后通过地址才能访问到对象本身。注意:不论迭代器的内部实现如何,引用语义的容器要访问对象总是分为上述两步 **iterator,而值语义的对象(即元素本身)访问只要一步 *iterator。
  • 根据容器策略,减少对象引用记数,或者(当对象彻底交由容器管理时)析构元素

其中红字部分是你遗漏的。引用计数的存在,造成了在对象构造和析构上各多了一步操作;特别是上面的第二条,造成了遍历容器的时间是基于值语义的容器的约两倍,在很多情况下(比如容器遍历是主要处理的情况)使得引用语义容器的效率会远远低于同样的值语义容器的效率。

引用语义的主要特点是不拷贝引用对象。如果不必要的拷贝对象的消耗在问题的处理中占据矛盾的主要方面,这种情况下使用引用语义才是合适的;否则,和使用值语义的容器相比,使用引用语义容器也可能没有优势、甚至还会带来效率的降低。

论坛徽章:
0
14 [报告]
发表于 2006-10-02 02:43 |只看该作者
whyglinux

1、引用记数不是必需的。STL 容器生命周期结束后,所有保存的指向容器内对象的指针、引用和指向容器的迭代器均会失效,这里可以保证同样的策略,不需要引用记数。
2、确实要多一步间接,是我忽略了。但是,由于设计的不同,这个性能损失在某些情况下是可以通过回避使用迭代器而回避的。

  1. #include <cassert>
  2. #include <cstdlib>
  3. #include <cstdio>
  4. #include <vector>
  5. #include <algorithm>

  6. template <typename T>
  7. struct LordOfDestructor {
  8.     void operator() ( T* pobjt ) {
  9.         delete pobjt;
  10.     }
  11. };

  12. template <typename T>
  13. struct ObjFactory {
  14.     static T *newInstance() {
  15.         return new T();
  16.     }
  17.    
  18.     static T *clone (const T& obj) {
  19.         return new T(obj);
  20.     }
  21. };

  22. template <typename T,
  23.           typename Dtor = LordOfDestructor<T>,
  24.           typename Factory = ObjFactory<T> >
  25. struct container : private std::vector< T* > {
  26.     typedef T& reference;
  27.     typedef const T& const_reference;
  28.     typedef std::vector< T* > base_t;
  29.    
  30.     struct iterator {
  31.         reference operator* () {
  32.             return **iter;
  33.         }
  34.         
  35.         typename base_t::iterator iter;
  36.         
  37.         iterator (typename base_t::iterator i)
  38.             :iter(i)
  39.             {}
  40.     };
  41.    
  42.     // simulation for STL semantics
  43.     T * add (const T& obj) {
  44.         T *rt = Factory::clone(obj);
  45.         base_t::push_back(rt);
  46.         return rt;
  47.     }
  48.    
  49.     // provide more conveniency for allocating and manipulating objects
  50.     T * add (T *p) {
  51.         base_t::push_back(p);
  52.         return p;
  53.     }
  54.    
  55.     void clear() {
  56.         for_each (base_t::begin(), base_t::end(), Dtor());
  57.     }
  58.    
  59.     iterator begin() {
  60.         return iterator(base_t::begin());
  61.     }
  62.    
  63.     ~container() {
  64.         clear();
  65.     }
  66.    
  67. };

  68. int main ()
  69. {
  70.     container<int> c;
  71.     int *p = c.add(2); // Since we'd never reallocate objects (as they're managed by Factory and Dtor)
  72.     container<int>::iterator iter = c.begin();
  73.     printf ("%d\n", *iter);
  74.     *p = 3;    // Pointer to obj returned previously would always be valid and safe to use (MT conditions excluded).
  75.     printf ("%d\n", *iter);
  76.     c.add(3);
  77.     iter = c.begin(); // iterator may be invalid
  78.     assert (*iter == 3);
  79.     assert ((void *)&*iter == (void *)p); // but original object always holds.
  80.     system("PAUSE");
  81. }
复制代码


Quite a few different characteristics between this and STL containers though..

论坛徽章:
0
15 [报告]
发表于 2006-10-02 11:08 |只看该作者
友情顶上面的代码
让建立和销毁独立,给用户选择的机会,
是很不错的做法,灵活。

论坛徽章:
0
16 [报告]
发表于 2006-10-03 11:53 |只看该作者
LS的LS的代码有很多的问题
首先是你实际上是特化了一个接受T但使用T*的vector,所以与其使用及其耦合的继承,还不如真去特化一个vector
第二,这个代码明显缺乏保护,如果T却是个类或者原生类型,那么以上代码可以工作,但如果T也是一个指针类型的话,程序也就离死不远了
第三,STL容器类本身会使用一个allocator,其实vector是定义为template <typename T, typename A = std::allocator<T> >的,也就是说,应当使用自己的分配器,而不是继承
第四,add函数通过暴露原生指针而直接揭露容器内部数据结构,这不尽违反了STL的值语义,更破坏了封装性
第五,让用户可以肆无忌惮的操纵容器内部数据成员,是极度不理智的行为,相当于裸奔

总之,STL容器的根本思想是值语义,这就相当于一个精美的礼品盒子,如果有人能凌空取物的话,主人会抓狂的;STL本身是希望范化而非特化,重用而非重写,标准委员会给出如此的库设计是有实际意义的,他给出了完成任务的绝佳模式,只是我们还没能全部挖掘出来。

ps. 如果希望容器析构时,内部元素也能自动析构,还是应该从shard_ptr上下手,这样还会有容器无关的优势

论坛徽章:
0
17 [报告]
发表于 2006-10-03 13:01 |只看该作者
lileding:

1、我的代码只是一个 demo snippet
2、我不是在特化 vector<T> 或 vector<T *>。请注意看,我用的是私有继承,换言之,是实现继承。这里 vector 只是作为一个方便的内存管理工具。换作直接操作 T *[] 也可以。
3、自己提供 allocator 的问题更多——首先一个问题就是你无法判断容器是在移动对象还是真的复制对象。我所提出的值语意带来的性能问题根本没有解决,甚至没有回避。另外,根据 SGI STL 文档
The details of the allocator interface are still subject to change, and we do not guarantee that specific member functions will remain in future versions.

你让我怎么去信任它?
4、我从开始就声明我这是一个指针语意容器,使用 T* 作为参数当然不算『暴露内部数据结构』。相反,通过允许用户指定 Factory 和 Dtor,使得容器和用户可以使用统一的内存分配机制,允许用户直接向容器『交付』对象,提供的是 STL 所完全不能达到的灵活性。
5、是不是让用户操作一个经由 new 创建对象得到的指针就算让用户『肆无忌惮的操纵 libc / libstdc++ 内部数据成员』了呢?

总之,我从一开始就说过我是排斥 STL 值语意的,而我的容器是完全基于指针/引用语意设计,根本没有考虑 STL 兼容。一个东西放在容器里,那么他就不会凭空消失或者乾坤大挪移——如果我把一个东西放在某处,过一会发现他竟然消失了,我才会抓狂。
盲目崇拜 std:: 是缺乏 pragmatic 精神的。

ps. shared_ptr 只是把原生指针包装得到一个值语意的间接层,纯属为了适应 STL 的拙劣设计而不得已为之。而且,如 whyglinux 所说,ref count 带来的效率影响是不能忘记的。

[ 本帖最后由 wolf0403 于 2006-10-3 13:08 编辑 ]

论坛徽章:
0
18 [报告]
发表于 2006-10-03 13:50 |只看该作者
看来分歧所在就是我更倾向于容器的值语义,容器于我更是放东西的盒子而不是放标签的抽屉,我个人难于接受一个指针语义的容器
从值语义的角度看,如果你把一个东西用盒子给出的方法放入后,不通过盒子就释放他,这明显是个疯狂的行为。

是不是让用户操作一个经由 new 创建对象得到的指针就算让用户『肆无忌惮的操纵 libc / libstdc++ 内部数据成员』了呢?

显然这不是,因为new/delete是libstdc++给出的接口;而delete container.add(new int(10))就分别使用了两个不同的接口。当然你说他是指针语义那另当别论,只是我始终认为container这类东西还是值语义的好

至于allocator,我也并不认为自己做个allocator是什么好事;但用自己的代码去完成语言已经提供的东西更不好

至于效率问题,要么充分相信程序运可以完美的处理内存,要么总得有个机制去标识内存的使用,引用计数即使是有前面所说的额外消耗,甚至还有循环引用,但似乎还没有什么更好的自动内存记录的方式

对于std:: 要知道,标准库就是C++语言的一部分,std和new一样都是内置方法。你说我崇拜,我只能说,我在用这个语言

[ 本帖最后由 lileding 于 2006-10-3 13:56 编辑 ]

论坛徽章:
0
19 [报告]
发表于 2006-10-03 14:02 |只看该作者
呵。返回对象装入容器后的所在地址的指针,并不是说我鼓励用户去手工释放这个对象。我前面的帖子也提到,这个对象是『交付』给容器的。换言之,加入容器之后,用户虽然可以使用这个对象,但是不应该去释放这个对象。我的 class 只是一个 snippet。完整的版本应该加入 iterator 和容器的 erase 方法,通过容器和迭代器管理对象声明。使用时可以直接操作对象,避免二次引用带来的效能损失。

论坛徽章:
0
20 [报告]
发表于 2006-10-03 14:09 |只看该作者
http://topic.csdn.net/T/20040318/16/2857938.html
这里有我一个很久之前的讨论。当时的想法很不成熟,但是最后的结论和今天仍然是一样的。……
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP