cpp03_no_res_transferring
- tags
- cpp_move_semantics
c++03 只有拷贝,没有转移
- 资源转移是很常见的需求,在c++03中却没有原生的转移机制,只有拷贝语义。想要转移资源,可以通过指针或自定义函数模拟,但都各有缺点,并不完美。
问题 1:拷贝有时性能很差
- 现在有个类 Datas 代表生产记录,存储部需要通过 archiveToPolice 接口归档资料。原意是资料移交给存储后,资料的所有权也一并转移;但在C++03中,只能通过拷贝资料给存储部门,当数据数量达到千万级别时,拷贝非常耗时。
class Datas {
public:
int* data;
size_t size;
Datas(size_t n) : data(new int[n]), size(n) {
std::cout << "采集 " << n << " 条记录\n";
}
~Datas() {
delete[] data;
}
Datas(const Datas& o) : data(new int[o.size]), size(o.size) {
for (size_t i = 0; i < size; ++i)
data[i] = o.data[i];
std::cout << "拷贝 — 重新采集 " << size << " 条记录\n";
}
};
// 存储部门要的是一个对象,不是指针
// 接口设计按值传递,符合"数据归我管"的语义
void archiveToPolice(Datas d) {
std::cout << "存储部门归档 " << d.size << " 条记录\n";
// d 离开作用域,数据归存储部门管理,由其析构
}
int main() {
std::cout << "===== 采集生产资料 =====\n";
Datas myData(10000000); // 1000 万条记录
std::cout << "\n===== 提交给存储部门 =====\n";
std::cout << "接口要求传入对象(按值传递),表示数据所有权移交:\n";
archiveToPolice(myData); // C++03:只能拷贝!
return 0;
}
- 矛盾点:
- 存储部门要的是对象,不是指针 — 接口设计合理
- 本意:我的资料以后归存储部门管,我不再持有
- 实际:C++03 只能拷贝 — 复制了 1000 万条记录
- 数据有两份,谁的算数?
通过transfer函数模拟转移
- 当然,我们可以通过实现transfer函数转移所有权到新对象,但实际使用并不优雅
- transfer()函数利用swap技巧避免了深拷贝
- 需要提前构造空对象
- 每个类需要自己实现transfer()函数
- 不能自动调用,需要手动调用,在本应该转移的场景却失效了(如临时对象资源的转移)
class Datas {
public:
...
// C++03 的 workaround:把另一个对象的资源"窃取"过来
void transfer(Datas& other) {
std::swap(data, other.data);
std::swap(size, other.size);
// 现在 other 持有本对象的旧资源,析构时负责释放;
// 本对象持有 other 的资源——通常是空的或被偷来的
}
};
int main() {
Datas myData(10000000);
// ... 填充数据 ...
Datas policeData(0); // 1. 先构造一个空壳
myData.transfer(policeData); // 2. 手动转移资源
// myData 现在是空壳(data == nullptr)
// policeData 持有原来的 1000 万条记录
}
问题 2:有些资源只能转移,不能拷贝
- 数据库连接是独占资源,如果让两个对象持有同一个连接,在释放时就会重复关闭。在c++03中用RAII管理连接,就要禁止拷贝。
RAII版本
class DBConn_NoCopy {
FakeDB* db_;
public:
DBConn_NoCopy(const char* host, int port,
const char* user, const char* pass)
: db_(db_connect(host, port, user, pass)) {}
~DBConn_NoCopy() { db_close(db_); }
void query(const char* sql) { db_query(db_, sql); }
private:
DBConn_NoCopy(const DBConn_NoCopy&);
DBConn_NoCopy& operator=(const DBConn_NoCopy&);
};
void demo_v1() {
DBConn_NoCopy conn("localhost", 3306, "root", "123456");
conn.query("SELECT 1");
conn.query("SELECT 2");
} // 离开作用域,析构函数自动关闭连接
// ✅ RAII 自动释放
// ✅ 独占,编译器防止共享
// ❌ 无法转移所有权 —— 不能从工厂函数返回,不能放入连接池
用指针复用连接
- 用RAII解决了资源释放的问题,但对象不能拷贝,如果想复用连接,只能用指针
- 一旦用指针,就可能引入各种问题
- 比如,有些情况是想将连接的所有权转移到其他模块,有些时候只是借用到其他模块,而指针是通用类型,没法表达这2种情况的不同,可能会让代码出错
class DBConn_Raw {
FakeDB* db_;
public:
DBConn_Raw(const char* host, int port,
const char* user, const char* pass)
: db_(db_connect(host, port, user, pass)) {}
DBConn_Raw(FakeDB* db) : db_(db) {}
~DBConn_Raw() { db_close(db_); }
void query(const char* sql) { db_query(db_, sql); }
FakeDB* release() {
FakeDB* tmp = db_;
db_ = NULL;
return tmp;
}
private:
DBConn_Raw(const DBConn_Raw&);
DBConn_Raw& operator=(const DBConn_Raw&);
};
// 连接池
struct ConnPool {
std::vector<FakeDB*> idle_;
void add(FakeDB* db) { idle_.push_back(db); }
FakeDB* borrow() {
if (idle_.empty()) return NULL;
FakeDB* db = idle_.back();
idle_.pop_back();
return db;
}
};
// 看签名,分不清是"借用"还是"接管"
void handleRequest_v2(FakeDB* db) {
// 看这个声明,你能判断:
// - 我会不会 delete db?
// - db 可以为 NULL 吗?
// - 调用完之后 db 还活着吗?
// → 答案全在文档里,编译器不关心
db_query(db, "SELECT * FROM orders WHERE user_id = 42");
}
void demo_v2() {
// ---------- 正常流程 ----------
ConnPool pool;
{
DBConn_Raw conn("localhost", 3306, "root", "123456");
FakeDB* raw = conn.release();
pool.add(raw);
}
FakeDB* db = pool.borrow();
if (db) {
handleRequest_v2(db);
pool.add(db); // 记得还
}
// ---------- 隐患 1:忘了归还 ----------
{
FakeDB* db2 = pool.borrow();
handleRequest_v2(db2);
// 忘了 pool.add(db2) → 连接泄漏
// 编译器:没报错。运行时:没报错。上线:内存慢慢涨。
}
// ---------- 隐患 2:double free ----------
{
FakeDB* db3 = pool.borrow();
DBConn_Raw wrapper(db3); // RAII 包装
// wrapper 析构 → db_close(db3) → free
// 但 pool 里还存着这个悬空指针
}
// ---------- 隐患 3:签名不表达意图 ----------
// void handleRequest_v2(FakeDB* db)
//
// 这个签名在 C++03 里可能是三种意思:
// (a) "借用,别释放" void borrow(T* p);
// (b) "接管,你负责释放" void take(T* p);
// (c) "可能为空的引用" void maybe(T* p);
//
// 意图丢在类型系统之外,全靠命名、注释、团队约定。
}
// ✅ 可以转移
// ❌ 安全性靠自觉
// ❌ 意图不明确
总结
| 场景 |
需求 |
C++03 方案 |
代价 |
| Datas(数据容器) |
移交所有权,避免深拷贝 |
transfer 函数 |
需要空壳对象,每个类自己写;不像拷贝函数,编译器不能自动触发 |
| DBConn(独占资源) |
RAII + 所有权可转移 |
裸指针 |
放弃 RAII,生命周期靠自觉 |
- 两个案例指向同一个空白: C++03 缺少一种语言级别的"转移"语义 。
- 拷贝是语言内置的,转移只能靠约定模拟。
- C++11 的移动语义把这个空白填补了, 带来以下的优势: