Skip to content

🧱 Pimpl Idiom Structural

Vũ khí bí mật của Qt, Boost, và HPN Tunnel SDK.

Ẩn chi tiết implementation khỏi headers, ổn định ABI, và giảm thời gian biên dịch.

Vấn đề: Rò rỉ Implementation

Header C++ Cổ điển

cpp
// database_client.hpp — ❌ RÒ RỈ IMPLEMENTATION
#include <mysql/mysql.h>      // MySQL headers bị lộ ra ngoài!
#include <memory>
#include <string>
#include <vector>

class DatabaseClient {
public:
    DatabaseClient(const std::string& connection_string);
    ~DatabaseClient();
    
    std::vector<Row> Query(const std::string& sql);

private:
    MYSQL* connection_;           // Kiểu MySQL trong header!
    MYSQL_RES* last_result_;      // Thêm kiểu MySQL nữa!
    std::string connection_str_;
    bool is_connected_;
    int retry_count_;
    // ... 20 thành viên private khác
};

💀 CÁC VẤN ĐỀ

  1. Người dùng phải #include <mysql/mysql.h> — Dù họ không dùng MySQL trực tiếp!
  2. Thay đổi private members → Tất cả client phải recompile
  3. Phá vỡ ABI → Thêm member thay đổi kích thước class
  4. Thời gian biên dịch dài → MySQL headers rất lớn

Giải pháp Pimpl

┌─────────────────────────────────────────────────────────────────────────┐
│                    KIẾN TRÚC PIMPL IDIOM                                │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   HEADER CÔNG KHAI (database_client.hpp)                                │
│   ────────────────────────────────────                                  │
│   ┌─────────────────────────────────────────────────────────────────┐  │
│   │  class DatabaseClient {                                         │  │
│   │  public:                                                        │  │
│   │      DatabaseClient(const std::string& conn);                   │  │
│   │      ~DatabaseClient();                                         │  │
│   │      std::vector<Row> Query(const std::string& sql);            │  │
│   │  private:                                                       │  │
│   │      struct Impl;                      // Forward declaration   │  │
│   │      std::unique_ptr<Impl> pimpl_;     // Con trỏ mờ            │  │
│   │  };                                                             │  │
│   └─────────────────────────────────────────────────────────────────┘  │
│                                                                         │
│   IMPLEMENTATION RIÊNG TƯ (database_client.cpp)                         │
│   ────────────────────────────────────────────                          │
│   ┌─────────────────────────────────────────────────────────────────┐  │
│   │  #include <mysql/mysql.h>  // Chỉ trong .cpp!                   │  │
│   │  struct DatabaseClient::Impl {                                  │  │
│   │      MYSQL* connection_;                                        │  │
│   │      MYSQL_RES* last_result_;                                   │  │
│   │      std::string connection_str_;                               │  │
│   │      bool is_connected_;                                        │  │
│   │      // Tất cả private members ở đây                            │  │
│   │  };                                                             │  │
│   └─────────────────────────────────────────────────────────────────┘  │
│                                                                         │
│   → Header SẠCH (không phụ thuộc MySQL)                                 │
│   → Thêm/xóa members mà không phá vỡ ABI                                │
│   → Tường lửa compile-time (thay đổi không lan truyền)                  │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Implementation Hoàn chỉnh

Header (API Công khai)

cpp
// database_client.hpp
#pragma once
#include <memory>
#include <string>
#include <vector>

struct Row;  // Forward declare kiểu kết quả

class DatabaseClient {
public:
    explicit DatabaseClient(const std::string& connection_string);
    ~DatabaseClient();
    
    // Move operations (cần thiết cho unique_ptr với kiểu incomplete)
    DatabaseClient(DatabaseClient&&) noexcept;
    DatabaseClient& operator=(DatabaseClient&&) noexcept;
    
    // API Công khai
    bool Connect();
    void Disconnect();
    std::vector<Row> Query(const std::string& sql);
    int Execute(const std::string& sql);

private:
    struct Impl;  // Chỉ forward declaration!
    std::unique_ptr<Impl> pimpl_;
};

Implementation (Chi tiết Riêng tư)

cpp
// database_client.cpp
#include "database_client.hpp"
#include <mysql/mysql.h>  // MySQL chỉ ở đây!

struct DatabaseClient::Impl {
    MYSQL* connection = nullptr;
    MYSQL_RES* last_result = nullptr;
    std::string connection_str;
    bool is_connected = false;
    int retry_count = 0;
    
    Impl(const std::string& conn) : connection_str(conn) {
        connection = mysql_init(nullptr);
    }
    
    ~Impl() {
        if (last_result) mysql_free_result(last_result);
        if (connection) mysql_close(connection);
    }
    
    bool DoConnect() {
        // Logic kết nối MySQL thực tế
        return mysql_real_connect(connection, 
            /* host, user, pass, db, port */) != nullptr;
    }
    
    std::vector<Row> DoQuery(const std::string& sql) {
        mysql_query(connection, sql.c_str());
        last_result = mysql_store_result(connection);
        // Chuyển đổi thành Row objects...
        return {};
    }
};

// Constructor/Destructor PHẢI ở trong .cpp (nơi Impl hoàn chỉnh)
DatabaseClient::DatabaseClient(const std::string& connection_string)
    : pimpl_(std::make_unique<Impl>(connection_string)) {}

DatabaseClient::~DatabaseClient() = default;  // Default hoạt động được!

DatabaseClient::DatabaseClient(DatabaseClient&&) noexcept = default;
DatabaseClient& DatabaseClient::operator=(DatabaseClient&&) noexcept = default;

// Chuyển tiếp lời gọi đến implementation
bool DatabaseClient::Connect() {
    return pimpl_->DoConnect();
}

void DatabaseClient::Disconnect() {
    pimpl_->is_connected = false;
    if (pimpl_->connection) {
        mysql_close(pimpl_->connection);
        pimpl_->connection = nullptr;
    }
}

std::vector<Row> DatabaseClient::Query(const std::string& sql) {
    return pimpl_->DoQuery(sql);
}

Tại sao unique_ptr Cần Xử lý Đặc biệt

cpp
// ❌ LỖI BIÊN DỊCH nếu không setup đúng
class Foo {
    struct Impl;
    std::unique_ptr<Impl> pimpl_;  // Lỗi: kiểu incomplete!
};
// unique_ptr cần kiểu hoàn chỉnh cho destructor

// ✅ GIẢI PHÁP: Định nghĩa destructor trong .cpp
// Header:
class Foo {
    struct Impl;
    std::unique_ptr<Impl> pimpl_;
public:
    ~Foo();  // Chỉ khai báo
};

// .cpp:
struct Foo::Impl { ... };
Foo::~Foo() = default;  // Bây giờ Impl đã hoàn chỉnh!

Giải thích Ổn định ABI

┌─────────────────────────────────────────────────────────────────────────┐
│                    ABI (Application Binary Interface)                   │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   KHÔNG CÓ PIMPL:                                                       │
│   ──────────────                                                        │
│   class Widget {                                                        │
│       int a;           // offset 0                                      │
│       double b;        // offset 8                                      │
│       std::string c;   // offset 16                                     │
│   };                                                                    │
│   sizeof(Widget) = 48 bytes                                             │
│                                                                         │
│   Thêm member mới:                                                      │
│   class Widget {                                                        │
│       int a;           // offset 0                                      │
│       int NEW_MEMBER;  // offset 4  ← CHÈN VÀO!                         │
│       double b;        // offset 8  ← DỊCH CHUYỂN!                      │
│       std::string c;   // offset 16 ← DỊCH CHUYỂN!                      │
│   };                                                                    │
│   sizeof(Widget) = 52 bytes  ← KÍCH THƯỚC THAY ĐỔI!                     │
│                                                                         │
│   → Binary biên dịch với header cũ SẼ CRASH với library mới!            │
│                                                                         │
│   CÓ PIMPL:                                                             │
│   ───────────                                                           │
│   class Widget {                                                        │
│       std::unique_ptr<Impl> pimpl_;  // offset 0, luôn 8 bytes          │
│   };                                                                    │
│   sizeof(Widget) = 8 bytes LUÔN LUÔN                                    │
│                                                                         │
│   → Thêm 100 members vào Impl → Binary client không thay đổi!           │
│   → Cập nhật SDK mà không cần recompile                                 │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Tường lửa Compile-Time

Không có Pimpl: Thay đổi mysql.h → Recompile TẤT CẢ clients Có Pimpl: Thay đổi mysql.h → Recompile CHỈ database_client.cpp

┌─────────────────────────────────────────────────────────────────────────┐
│                    SO SÁNH THỜI GIAN BIÊN DỊCH                          │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   Dự án với 500 file .cpp sử dụng DatabaseClient:                       │
│                                                                         │
│   KHÔNG CÓ PIMPL (thay đổi private member):                             │
│   • 500 files cần recompile                                             │
│   • Thời gian build: ~15 phút                                           │
│                                                                         │
│   CÓ PIMPL (thay đổi Impl):                                             │
│   • 1 file cần recompile (database_client.cpp)                          │
│   • Thời gian build: ~5 giây                                            │
│                                                                         │
│   → Incremental builds nhanh hơn 180 lần!                               │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Ví dụ Thực tế

Qt Framework

cpp
// Qt sử dụng Pimpl rộng rãi (gọi là "d-pointer")
class QWidget {
public:
    // ...
protected:
    QWidgetPrivate* d_ptr;  // "d-pointer"
};

// Tất cả private members trong QWidgetPrivate
// ABI ổn định qua các phiên bản Qt minor!

HPN Tunnel SDK

cpp
// hpn_tunnel.hpp (Header SDK Công khai)
class HPNTunnel {
public:
    HPNTunnel(const Config& config);
    ~HPNTunnel();
    
    void Connect();
    void Disconnect();
    size_t Send(const void* data, size_t len);

private:
    struct TunnelImpl;
    std::unique_ptr<TunnelImpl> impl_;
};

// Người dùng không cần OpenSSL, zlib, hay networking headers!
// Cập nhật SDK mà không cần recompile code người dùng

Thực hành Tốt nhất

┌─────────────────────────────────────────────────────────────────────────┐
│                    THỰC HÀNH TỐT NHẤT PIMPL                             │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   ✅ KHI NÀO SỬ DỤNG PIMPL                                              │
│   ─────────────────────                                                 │
│   • Public headers của Library/SDK                                      │
│   • Classes có nhiều private members                                    │
│   • Khi cần ổn định ABI                                                 │
│   • Khi có dependencies nặng                                            │
│                                                                         │
│   ❌ KHI NÀO KHÔNG NÊN SỬ DỤNG PIMPL                                    │
│   ───────────────────────                                               │
│   • Classes nhỏ, đơn giản                                               │
│   • Hot paths quan trọng hiệu năng (thêm indirection)                   │
│   • Classes implementation nội bộ                                       │
│   • Header-only libraries                                               │
│                                                                         │
│   💡 MẸO IMPLEMENTATION                                                 │
│   ──────────────────────                                                │
│   • Luôn định nghĩa destructor trong .cpp                               │
│   • Định nghĩa move operations trong .cpp                               │
│   • Cân nhắc std::aligned_storage cho buffer cố định                    │
│   • Sử dụng propagate_const<> cho const-correctness                     │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Bước tiếp theo

🔒 Singleton → — Thread-safe Singleton với Meyers' Method