28.More C++ Idioms - Execute-Around Pointer惯用法 and more

enter image description here

“Execute-Around Pointer” 是一个相对少见但极具设计美感的 C++ 惯用法,最初收录于《More C++ Idioms》wikibook 中。它体现了 C++ 语言在资源管理和行为注入(aspect-oriented behavior injection)方面的高可塑性。这个模式非常适合封装资源访问、日志、线程锁、事务控制等需要在调用目标函数前后执行固定动作的场景。


🧠 意图(Intent) 用一句话概括这个惯用法:

封装某个对象,使得对它的每次操作(函数调用)前后,自动执行一些附加操作。

你可以把它看作是一种AOP(面向切面编程)在 C++ 中的简化实现方式。常见用法包括:

  • 在访问某对象时自动加锁 / 解锁(线程同步)
  • 在访问某资源时自动记录日志(日志注入)
  • 在事务开始前/结束后自动提交或回滚(数据库事务)

🌟 类比:RAII 是作用于“生命周期”,而 Execute-Around Pointer 是作用于“每次调用” RAII(Resource Acquisition Is Initialization)模式会在构造 / 析构时执行特定逻辑:

    {
        std::lock_guard<std::mutex> lock(mtx);  // 构造时加锁,析构时自动释放
        // 临界区代码
   }

而 Execute-Around Pointer 则是对每次成员函数调用前后做处理,而不仅限于构造/析构

📦 实现方式(简单版本) 基本思想是写一个代理类,重载 operator->() 或 operator*(),使得在访问内部对象成员函数时,自动包裹前后操作。

示例:自动加锁访问共享资源

#include <iostream>
#include <mutex>
#include <memory>

class SharedResource {
public:
    void doWork() {
        std::cout << "Working on shared resource\n";
    }
};

// Execute-Around Pointer
template<typename T>
class LockedPtr {
public:
    LockedPtr(std::shared_ptr<T> ptr, std::mutex& mtx)
        : m_ptr(ptr), m_mtx(mtx) {
        m_mtx.lock();  // 前操作:加锁
    }

    ~LockedPtr() {
        m_mtx.unlock();  // 后操作:解锁
    }

    T* operator->() {
        return m_ptr.get();  // 转发成员调用
    }

private:
    std::shared_ptr<T> m_ptr;
    std::mutex& m_mtx;
};

// 工厂函数简化调用
template<typename T>
LockedPtr<T> make_locked(std::shared_ptr<T> ptr, std::mutex& mtx) {
    return LockedPtr<T>(ptr, mtx);
}

使用方式如下:

int main() {
    auto resource = std::make_shared<SharedResource>();
    std::mutex mtx;

    {
        auto locked = make_locked(resource, mtx);
        locked->doWork();  // 自动加锁/解锁
    }

    return 0;
}

输出:

Working on shared resource

🔍 关键点分析 特征 说明 封装行为 在 operator-> 里只允许在作用域中安全访问对象 透明调用 用户写 ptr->method(),无需关心锁机制 调用开销小 只包裹函数调用,不产生额外动态开销 高可扩展性 可以支持日志、事务、性能统计等用途

🧩 类似场景延伸 应用场景 Execute-Around 示例 数据库事务管理 自动 begin / commit / rollback 日志系统 每次函数调用记录日志 安全检查 调用前验证权限,调用后清理 性能监控 记录函数执行耗时 临时状态设置 例如切换 OpenGL 状态或设置线程本地参数 🛡️ 进阶技巧:结合 Lambda 或策略类 如果想让前后处理行为更加灵活,可以改写为策略注入或 lambda 包裹:

template<typename T, typename Pre, typename Post>
class AroundPtr {
public:
    AroundPtr(std::shared_ptr<T> ptr, Pre pre, Post post)
        : m_ptr(ptr), m_post(post) {
        pre();
    }

    ~AroundPtr() {
        m_post();
    }

    T* operator->() {
        return m_ptr.get();
    }

private:
    std::shared_ptr<T> m_ptr;
    Post m_post;
};

✅ 总结 Execute-Around Pointer 是一种结合了 RAII 和代理思想的惯用法,它的价值在于:

抽象并自动化执行一些访问前后需要重复写的逻辑; 让使用者写出简洁、结构清晰、可维护的代码; 避免逻辑遗漏(比如忘记释放锁); 具有良好的通用性,是某种“轻量级 AOP”。


如果你感兴趣,我可以再演示一个用这个惯用法实现“数据库事务”的完整例子,或者聊聊它和 C++20 coroutine 的一些有趣组合 😄 要不要来一个?

好的,来一个经典的 数据库事务(Database Transaction)场景,我们将使用 Execute-Around Pointer 惯用法自动控制事务的 begin() / commit() / rollback(),确保异常或错误时能够正确地回滚事务,而不需要用户手动去记得写这些流程控制逻辑。

💡 目标 用法像这样 👇:

{
    auto tx = make_transactional(db);
    tx->updateData();
    tx->insertRow();
    // tx.commit() happens automatically on success
}

如果在作用域中出现异常或逻辑失败,自动回滚事务。

🧱 模拟数据库类 先写一个简单的伪数据库类(演示用):

#include <iostream>
#include <memory>
#include <stdexcept>

class Database {
public:
    void beginTransaction() {
        std::cout << "[DB] BEGIN TRANSACTION\n";
    }

    void commitTransaction() {
        std::cout << "[DB] COMMIT TRANSACTION\n";
    }

    void rollbackTransaction() {
        std::cout << "[DB] ROLLBACK TRANSACTION\n";
    }

    void updateData() {
        std::cout << "[DB] Updating data...\n";
    }

    void insertRow() {
        std::cout << "[DB] Inserting row...\n";
    }
};

🧠 Execute-Around Pointer 实现:TransactionalPtr

template<typename T>
class TransactionalPtr {
public:
    TransactionalPtr(std::shared_ptr<T> db)
        : db_(db), committed_(false) {
        db_->beginTransaction();  // 自动开启事务
    }

    ~TransactionalPtr() {
        if (!committed_) {
            db_->rollbackTransaction();  // 自动回滚事务
        }
    }

    T* operator->() {
        return db_.get();  // 转发成员调用
    }

    void commit() {
        db_->commitTransaction();  // 用户显式调用 commit
        committed_ = true;
    }

private:
    std::shared_ptr<T> db_;
    bool committed_;
};

template<typename T>
TransactionalPtr<T> make_transactional(std::shared_ptr<T> db) {
    return TransactionalPtr<T>(db);
}

🧪 示例用法:测试正常情况和异常情况

int main() {
    auto db = std::make_shared<Database>();

    std::cout << "=== [Transaction 1: Normal Commit] ===\n";
    {
        auto tx = make_transactional(db);
        tx->updateData();
        tx->insertRow();
        tx.commit();  // 显式提交
    }

    std::cout << "\n=== [Transaction 2: Exception -> Rollback] ===\n";
    try {
        auto tx = make_transactional(db);
        tx->updateData();
        throw std::runtime_error("Oops, something went wrong!");
        tx.commit();  // 不会执行
    } catch (const std::exception& ex) {
        std::cout << "[EXCEPTION] " << ex.what() << "\n";
    }

    return 0;
}

✅ 输出结果

=== [Transaction 1: Normal Commit] ===
[DB] BEGIN TRANSACTION
[DB] Updating data...
[DB] Inserting row...
[DB] COMMIT TRANSACTION

=== [Transaction 2: Exception -> Rollback] ===
[DB] BEGIN TRANSACTION
[DB] Updating data...
[DB] ROLLBACK TRANSACTION
[EXCEPTION] Oops, something went wrong!

🎯 你学到了什么 事务控制用 TransactionalPtr 来自动管理; 默认自动回滚; 用户可选显式调用 .commit(); 无需记得写 try-catch 或 rollback,异常安全; 代码整洁,逻辑集中在事务作用域中,符合 C++ 的资源即行为的哲学。


需要我帮你扩展为更复杂场景(比如嵌套事务、RAII commit、日志记录)吗?或者改写为可配置策略版本?这惯用法空间挺大的~


这个惯用法确实确实漂亮,扩展性,可用范围如此广泛 今天先了解到这里,慢慢消化一下

评论