关于noncopyable和STL容器的思考

cpp中的资源管理类,甚至绝大多数类都不应该允许拷贝,其理由有

  • 拷贝是值语义,前后的对象是没有关联的,而我们往往打交道的是对象语义,为了保证语义不被破坏或滥用,我们应该禁止拷贝。
  • 同时,因为编译器的默认拷贝实质上是(memberwise)的,也就是说并不是真的拷贝资源,而是让这两个类拥有相同的成员,如果成员是用户自定义类类型并且拷贝行为正确,会调用其对应的拷贝函数,问题不大,但是在C语言中有很多用内置类型表示资源(比如文件描述符和文件指针),还有就是结构体本身也是没有拷贝构造/复制函数的(这种情况表现为bitwise,即逐字节拷贝),这样就可能导致会出现资源释放两次这样的诡异现象存在,而这些你也不可能不与之打交道(实际这是绝大数cpp程序员都需要经历的)。

因此如果一个类真的需要拷贝语义,那么应当显式提供Copy()API,因为我们的目的就是禁止编译器的隐式行为,这点在google code style中也有提及。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// In C++98
#if __cplusplus < 201103L
class noncopyable {
private: // Also it is default private
noncopyable(noncopy const&) {}
noncopyable& operator=(noncopy const&) {}
};
#else
// In C++11
class noncopyable {
public: // private is also ok
noncopyable(noncopyable const&) = delete;
noncopyable& operator=(noncopyable const&) = delete;
};
#endif

// 上面这两个类可以作为基类,让其他类private继承(public也可以,不过不符合传统的is a关系,避免引来误解)
// 除此之外,也可以使用宏,如下

// C++98
#if __cplusplus < 201103L
# define DISABLE_COPY(class_name) \
private:
class_name(class_name const&) {}
class_name& operator=(class_name const&) {}

#else
# define DISABLE_COPY(class_name) \
class_name(class_name const&) = delete; \
class_name& operator=(class_name const&) = delete;
#endif

但是我同时也一直在思考,这种不可拷贝的类构造的对象究竟怎样放入STL容器比较好?因为STL容器对于元素类型有个要求就是必须是CopyConstructible,也就是说这两者是互斥的,我们或许得引入中间层解决这个问题。

我们先来考虑C++98吧,那时候对象还没有移动语义。

考虑指针会不错,裸指针(或原生指针,raw pointer)是内置类型嘛,可以塞进容器,但是生命周期的管理很难受。

这里引入指针的所有权语义概念:

  • 共享语义:该对象被多个类或对象共享,生命周期直到最后使用者使用完(或没人使用它了)才释放
  • 独占语义:该对象被一个类或对象独占,生命周期完全有自己管理,同时,允许转让所有权

对于98来说,可以使用tr1,它是c++0x的草案标准库,有tr1::shared_ptr可以满足共享语义的需求

独占语义可以通过std::auto_ptr实现,它本身也允许拷贝,但是本质上是移动资源,转交资源的所有权,伪装成了拷贝。

这种trick让它能够塞进STL容器,但是由于其语义的不明确容易给使用者带来误解,在C++11的移动语义引入后就被std::unique_ptr给替代了。

我们将视角切换到C++11,这个时候C++标准引入了移动语义,同时带来了std::unique_ptrstd::shared_ptr

这两个完美符合我们的要求。除此之外,我们也可以将对象不经由指针包装直接塞进容器,只有我们给这个类定义了行为正确的移动构造函数和移动赋值函数(用自定义的swap()就可以轻松实现这两个函数,同时你也发现了,在11之前,移动是可以实现的,只要容器公开insert_move(T &)这样的API,容器先构造一个这样的对象,然后与参数交换,不过这样也有很多问题,比如如果没有默认构造也不能构造,然后即使构造了原来的也有资源,每次都得换出去释放,显得多余),所以移动语义算是C++11的一大进步,而且移动语义对于这种资源管理类如果是左值的话往往是需要显式调用std::move()才能触发移动构造/赋值函数的调用,所以并不会破坏语义。

总结

将不可拷贝类的对象塞进容器有如下方法:

  • 共享语义:std::shared_ptr(c++11),tr1::shared_ptr(c++98)
  • 独占语义:std::unique_ptr(c++11),std::auto_ptr(c++98)
  • (C++11):支持移动语义的类,通过容器提供的新API可以塞进去