posts

vnn01

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;
}
  • 矛盾点:
    1. 存储部门要的是对象,不是指针 — 接口设计合理
    2. 本意:我的资料以后归存储部门管,我不再持有
    3. 实际:C++03 只能拷贝 — 复制了 1000 万条记录
    4. 数据有两份,谁的算数?

通过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 的移动语义把这个空白填补了, 带来以下的优势:
    • 更好的可读性
    • 更高的安全性
    • 更高性能