最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

為什么現(xiàn)代?C++?庫都用?PIMPL?一場關于封裝、依賴與安全的演進

 更新時間:2026年02月15日 14:53:30   作者:Charlee44  
這篇文章主要介紹了為什么現(xiàn)代?C++?庫都用?PIMPL?一場關于封裝、依賴與安全的演進的相關資料,需要的朋友可以參考下

在 C++ 的工程實踐中,如何在保證資源安全管理的同時,又避免頭文件污染和不必要的編譯依賴?這個問題貫穿了現(xiàn)代 C++ 庫設計的核心。本文將沿著一條清晰的技術演進路徑,探討從 RAII 封裝出發(fā),歷經值語義、裸指針、智能指針等階段,最終走向 PIMPL(Pointer to Implementation) 這一成熟且優(yōu)雅的解決方案。

1. RAII——資源管理的基石

C++ 的核心哲學之一是 RAII(Resource Acquisition Is Initialization):資源(內存、文件句柄、網(wǎng)絡連接等)的生命周期應由對象的構造與析構自動管理。例如:

class FileHandle {
    FILE* fp;
public:
    FileHandle(const char* path) : fp(fopen(path, "r")) {}
    ~FileHandle() { if (fp) fclose(fp); }
};

RAII 讓資源管理變得安全:利用類對象的生命周期,在構造函數(shù)中申請資源,在析構函數(shù)中釋放資源。如果這個類對象是基于棧的值對象,那么就可以自動實現(xiàn)資源的管理。因此,在現(xiàn)代 C++ 中,相比傳統(tǒng)的指針語義,更加提倡使用基于 RAII 的值語義。

2. 值語義的誘惑與代價

但是,當我們把這種思想用于封裝復雜組件(如 ONNX 模型會話、數(shù)據(jù)庫連接池)時,問題出現(xiàn)了。理想情況下,我們希望像使用 std::string 一樣,用“值語義”操作一個封裝對象:

class Embedder {
    Ort::Session session; // 值成員
public:
    std::vector<float> embed(const std::string& text);
};

這看起來非常簡潔、高效、符合現(xiàn)代 C++ 風格。但也有另外一個問題:破壞了封裝,導致不必要的環(huán)境依賴。最直觀的問題就是 Ort::Session 的完整定義必須出現(xiàn)在頭文件中,這意味著使用者必須包含 onnxruntime ,而這個頭文件可能重達數(shù) MB ,依賴數(shù)十個系統(tǒng)庫。這就會造成如下問題:

  • 編譯時間暴增,微小的改動都需要編譯很長的時間。
  • 頭文件耦合嚴重,調用者使用不方便,甚至造成環(huán)境污染。
  • ABI 極其脆弱,內部改動導致所有用戶重編譯。

3. 指針語義的回退

為了解耦,一個比較好的辦法就是使用前置聲明 + 指針語義:

// header
class SessionImpl; // 前置聲明
class Embedder {
    SessionImpl* pimpl;
public:
    Embedder();
    ~Embedder(); // 必須手動 delete
};

這樣做確實切斷了編譯依賴,但也引入了新的問題。那就是需要按照 RAII 原則寫好構造函數(shù)和析構函數(shù)。而一旦要寫析構函數(shù),也往往意味著需要寫另外四個特殊的成員函數(shù):

  • 拷貝構造函數(shù)(Copy Constructor)
  • 拷貝賦值運算符(Copy Assignment Operator)
  • 移動構造函數(shù)(Move Constructor)
  • 移動賦值運算符(Move Assignment Operator)

這樣做要寫非常多的樣板代碼,而且也很容易出問題。為了封裝犧牲安全,得不償失。

4. 使用智能指針

使用裸指針又麻煩又不安全,那么就可以使用 C++11 引入的智能指針:std::unique_ptr 和 std::shared_ptr;智能指針同樣是基于 RAII 的:

class SessionImpl;
class Embedder {
    std::unique_ptr<SessionImpl> pimpl;
};

這里為什么使用 std::unique_ptr 而不使用 std::shared_ptr 呢?其實也可以,不過在現(xiàn)代 C++ 中,更推薦使用 std::unique_ptr 。std::shared_ptr 是用來共享資源的所有權,會對引用資源進行計數(shù),但是有可能會造成相互循環(huán)引用造成不能釋放資源的問題;而std::unique_ptr 則表示獨占資源的所有權,不僅開銷更低(無引用計數(shù)),也更加安全(只能通過 std::move 轉移所有權 )。

不過有一點需要注意:std::unique_ptr 和 std::shared_ptr 在處理不完整類型(incomplete type)時的行為截然不同。具體來說,當在頭文件中使用前置聲明(如 class Impl;)并用智能指針持有它時,Impl 是一個不完整類型。

  • std::shared_ptr 可以安全地在頭文件中默認析構,因為它在構造時(通常在 .cpp 文件中)會捕獲一個完整的刪除器(deleter),即使析構發(fā)生在頭文件上下文中,也能正確調用 delete
  • 而 std::unique_ptr 的刪除器是其類型的一部分(通常是默認的 std::default_delete<Impl>),它要求在析構點(即類的析構函數(shù)被實例化的地方)Impl 必須是完整類型。如果在頭文件中寫 ~Embedder() = default;,此時 Impl 仍是不完整的,編譯器可能不會報錯,但會導致未定義行為(通常是鏈接失敗或運行時崩潰)。

因此,使用 std::unique_ptr<Impl> 時,必須將主類的析構函數(shù)定義移到 .cpp 文件中,確保 Impl 已被完整定義:

// Embedder.cpp
class Embedder::Impl {
    // 完整定義...
};

Embedder::~Embedder() = default; // ? 此時 Impl 完整,安全析構

5. 封裝與效率的平衡:PIMPL

使用智能指針雖然好,但是總歸是比不上值語義方便。當類中只有一個需要隱藏的成員還好,如果有很多個需要隱藏的成員,每一個都寫前置聲明,并用智能指針來管理,那就實在太繁瑣了。并且,從編程品味上來說,C++ 智能指針的寫法說不上優(yōu)雅:智能指針是由傳染性的,當滿屏都是 std::shared_ptr 或者 std::unique_ptr 的時候,實在很影響閱讀性。

另外,作為對外的接口,最好是提供像 Java / C# 那樣的接口,C++ 的純虛函類也行,隱藏掉所有的細節(jié),包括私有函數(shù)和數(shù)據(jù)成員。這樣有非常多的好處:

  • 最小化依賴環(huán)境,提升編譯速度。
  • 調用者使用方便,不會污染環(huán)境。
  • ABI 穩(wěn)定,可以只更新庫而不用更新整個程序。

那么要怎么進行優(yōu)化呢?很簡單,我們可以實現(xiàn)一個名為 Impl 的類中類 ,使用std::unique_ptr進行管理。Impl 是實現(xiàn)在 cpp 中的,可以將一切實現(xiàn)的細節(jié),比說私有函數(shù)和數(shù)據(jù)成員,都放在這個 Impl 中。更重要的是,Impl 中的數(shù)據(jù)成員完全可以使用值類型!如下所示:

// 頭文件
class Embedder {
    class Impl;
    std::unique_ptr<Impl> impl;
public:
    Embedder(const std::string& model);
    ~Embedder(); // 聲明但不在頭文件定義!
    std::vector<float> embed(std::string_view text) const;
};
// 源文件
class Embedder::Impl {
    Ort::Session session;
    hf::Tokenizer tokenizer;
    int64_t dim;
public:
    Impl(const std::string& path, const hf::Tokenizer& tok) 
        : session(...), tokenizer(tok) { /* init */ }
    std::vector<float> embed(std::string_view text) const { /* ... */ }
};

Embedder::Embedder(const std::string& path) 
    : impl(std::make_unique<Impl>(path, global_tokenizer)) {}

Embedder::~Embedder() = default; // 此時 Impl 完整,安全!

這個實現(xiàn),就是所謂的 PIMPL(Pointer to IMPLementation)慣用法,也常被稱作 “編譯防火墻”(Compilation Firewall) 或 “Opaque Pointer” 模式。不得不說,這種 PIMPL 設計模式確實精妙——它在安全性、封裝性、編譯效率與接口簡潔性之間取得了近乎完美的平衡,既堅守了 RAII 的資源管理原則,又有效隔離了實現(xiàn)細節(jié),堪稱現(xiàn)代 C++ 工程實踐中“高內聚、低耦合”的典范。

6. 沒有銀彈,只有權衡

PIMPL 使用了前置聲明。是否使用前置聲明一直是 C++ 中比較爭議的一點,Qt 遵循前置聲明的原則實現(xiàn)了非常強大、優(yōu)雅且高效的 C++ 運行時框架。Google 則經歷了從推薦使用前置聲明到不推薦使用前置聲明的轉變。個人認為,PIMPL 解決的就是 C++ 中兩個重要原則矛盾的問題:

  • 推薦使用值語義,但是會引入更多環(huán)境依賴
  • 封裝需要盡可能隱藏不必要的細節(jié)

如果兩者只能選擇其中一個,那么還是盡量使用值語義的原則更加重要,畢竟這涉及到安全問題,而資源管理的安全問題貫穿 C++ 程序的始終。事實上,如果不是提供對外接口,或者實現(xiàn)比較小,那么直接使用值語義即可(第2節(jié)中的內容)——值語義永遠是最簡潔安全的實現(xiàn)。

另外,如果實現(xiàn) C++20 Modules ,那么就不必要使用 PIMPL 了,完全可以回歸值語義實現(xiàn),因為 C++20 Modules 在語言層面已經實現(xiàn)了 PIMPL 的諸多優(yōu)點。

7. 示例代碼

最后放出筆者自己實現(xiàn)的基于 PIMPL 的嵌入器的完整代碼供讀者參考:

// BgeOnnxEmbedder.h
#pragma once

#include <memory>
#include <string>
#include <vector>

namespace embedding {

namespace hf {
class Tokenizer;
}

class BgeOnnxEmbedder {
 public:
  explicit BgeOnnxEmbedder(const std::string& modelPath,
                           const hf::Tokenizer& tokenizer);
  ~BgeOnnxEmbedder();

  const int64_t& EmbeddingDim() const;

  std::vector<float> Embed(const std::string& text) const;

 private:
  class Impl;  // 前向聲明
  std::unique_ptr<Impl> impl;
};

}  // namespace embedding
//BgeOnnxEmbedder.cpp
#include "BgeOnnxEmbedder.h"

#include <onnxruntime_cxx_api.h>

#include "HfTokenizer.h"
#include "Util/StringEncode.h"

namespace embedding {

class BgeOnnxEmbedder::Impl {
 public:
  Ort::Env& GetOrtEnv() {
    static Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "BgeOnnxEmbedder");
    return env;
  }

  const int64_t& EmbeddingDim() const { return embeddingDim; }

  explicit Impl(const std::string& modelPath, const hf::Tokenizer& tokenizer)
      : session{GetOrtEnv(),
#ifdef _WIN32
                util::StringEncode::Utf8StringToWideString(modelPath).c_str(),
#else
                modelPath.c_str(),
#endif
                Ort::SessionOptions()},
        memInfo{Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU)},
        tokenizer(tokenizer),
        embeddingDim(0) {

    //
    const auto& outputInfo = session.GetOutputTypeInfo(0);
    const auto& tensorInfo = outputInfo.GetTensorTypeAndShapeInfo();
    const auto& shape = tensorInfo.GetShape();

    // 假設輸出是 [batch, seq, dim] 或 [batch, dim]
    // 我們取最后一個非 -1 的維度
    for (auto it = shape.rbegin(); it != shape.rend(); ++it) {
      if (*it != -1) {
        embeddingDim = *it;
        break;
      }
    }

    if (embeddingDim == 0) {
      throw std::runtime_error(
          "Failed to infer embedding dimension from ONNX model.");
    }
  }

  std::vector<float> Embed(const std::string& text) const {
    hf::Tokenizer::ResultPtr result = tokenizer.Encode(text);
    if (!result) {
      throw std::runtime_error("tokenizer_encode failed");
    }

    // 定義張量維度
    int64_t seqLen = static_cast<int64_t>(result->length);
    std::vector<int64_t> inputShape = {1, seqLen};
    size_t dataByteCount = sizeof(int64_t) * seqLen;

    Ort::Value inputIdsTensor = Ort::Value::CreateTensor(
        memInfo.GetConst(), result->input_ids, dataByteCount, inputShape.data(),
        inputShape.size(),
        ONNXTensorElementDataType::ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64);

    Ort::Value attentionMaskTensor = Ort::Value::CreateTensor(
        memInfo.GetConst(), result->attention_mask, dataByteCount,
        inputShape.data(), inputShape.size(),
        ONNXTensorElementDataType::ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64);

    Ort::Value tokenTypeIdsTensor = Ort::Value::CreateTensor(
        memInfo.GetConst(), result->token_type_ids, dataByteCount,
        inputShape.data(), inputShape.size(),
        ONNXTensorElementDataType::ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64);

    // 輸入名必須與模型定義一致
    const char* inputNames[] = {"input_ids", "attention_mask",
                                "token_type_ids"};
    const char* outputNames[] = {"last_hidden_state"};

    // 把三個輸入張量放進數(shù)組
    std::vector<Ort::Value> inputs;
    inputs.push_back(std::move(inputIdsTensor));
    inputs.push_back(std::move(attentionMaskTensor));
    inputs.push_back(std::move(tokenTypeIdsTensor));

    // 執(zhí)行推理
    auto outputs = session.Run(Ort::RunOptions(),  // 運行選項(通常 nullptr)
                               inputNames,         // 輸入名數(shù)組
                               inputs.data(),  // 輸入張量數(shù)組
                               inputs.size(),  // 輸入數(shù)量(3)
                               outputNames,    // 輸出名數(shù)組
                               1               // 輸出數(shù)量(1)
    );

    // 獲取輸出信息
    auto& output_tensor = outputs[0];
    auto output_shape = output_tensor.GetTensorTypeAndShapeInfo().GetShape();
    if (output_shape.size() != 3 || output_shape[0] != 1) {
      throw std::runtime_error("Unexpected output shape");
    }

    // 獲取輸出張量的原始 float 指針
    const float* outputData = outputs[0].GetTensorData<float>();

    // 提取 [CLS] token 的 embedding(第0個token)
    int64_t hiddenSize = output_shape[2];
    std::vector<float> embedding(outputData, outputData + hiddenSize);

    // L2 歸一化(BGE 要求)
    float norm = 0.0f;
    for (float v : embedding) norm += v * v;
    norm = std::sqrt(norm);
    if (norm > 1e-8) {
      for (float& v : embedding) v /= norm;
    }

    return embedding;
  }

 private:
  mutable Ort::Session session;
  Ort::MemoryInfo memInfo;
  const hf::Tokenizer& tokenizer;
  int64_t embeddingDim;
};

BgeOnnxEmbedder::BgeOnnxEmbedder(const std::string& modelPath,
                                 const hf::Tokenizer& tokenizer)
    : impl(std::make_unique<Impl>(modelPath, tokenizer)) {}

BgeOnnxEmbedder::~BgeOnnxEmbedder() = default;  // 此時 Impl 已定義,可安全析構

const int64_t& BgeOnnxEmbedder::EmbeddingDim() const {
  return impl->EmbeddingDim();
}

std::vector<float> BgeOnnxEmbedder::Embed(const std::string& text) const {
  return impl->Embed(text);
}

}  // namespace embedding

到此這篇關于為什么現(xiàn)代 C++ 庫都用 PIMPL?一場關于封裝、依賴與安全的演進的文章就介紹到這了,更多相關C++ 庫都用 PIMPL內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • c語言中unsigned修飾符的使用

    c語言中unsigned修飾符的使用

    在C語言中,unsigned是一種無符號整數(shù)修飾符,本文主要介紹了c語言中unsigned修飾符的使用,具有一定的參考價值,感興趣的可以了解一下
    2023-11-11
  • C語言每日練習之字符串反轉

    C語言每日練習之字符串反轉

    這篇文章主要介紹了C語言字符串反轉,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2021-11-11
  • C語言實現(xiàn)2048游戲代碼

    C語言實現(xiàn)2048游戲代碼

    這篇文章主要為大家詳細介紹了C語言實現(xiàn)2048游戲代碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-05-05
  • C++ move 的作用詳解及陷阱最佳實踐

    C++ move 的作用詳解及陷阱最佳實踐

    文章詳細介紹了C++中的`std::move`函數(shù)的作用,包括為什么需要它、它的本質、典型使用場景、以及一些常見陷阱和最佳實踐,感興趣的朋友跟隨小編一起看看吧
    2025-12-12
  • IOS開發(fā)之UIScrollView實現(xiàn)圖片輪播器的無限滾動

    IOS開發(fā)之UIScrollView實現(xiàn)圖片輪播器的無限滾動

    這篇文章主要介紹了IOS開發(fā)之UIScrollView實現(xiàn)圖片輪播器的無限滾動的相關資料,需要的朋友可以參考下
    2017-07-07
  • C/C++中的靜態(tài)變量注意事項

    C/C++中的靜態(tài)變量注意事項

    本文主要介紹了C/C++中的靜態(tài)變量注意事項,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2022-07-07
  • C++基于prim實現(xiàn)迷宮生成

    C++基于prim實現(xiàn)迷宮生成

    這篇文章主要為大家詳細介紹了C++基于prim實現(xiàn)迷宮生成,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-05-05
  • c++基礎語法:構造函數(shù)初始化列表

    c++基礎語法:構造函數(shù)初始化列表

    構造函數(shù)需要初始化的數(shù)據(jù)成員,不論是否顯示的出現(xiàn)在構造函數(shù)的成員初始化列表中,都會在該處完成初始化,并且初始化的順序和其在聲明時的順序是一致的,與列表的先后順序無關
    2013-09-09
  • C語言輸入一個數(shù)判斷是否為素數(shù)的多種方法

    C語言輸入一個數(shù)判斷是否為素數(shù)的多種方法

    素數(shù)是只能被1和它自己本身整除,不能被其他自然數(shù)整除的大于1的正整數(shù),下面這篇文章主要給大家介紹了關于C語言輸入一個數(shù)判斷是否為素數(shù)的多種方法,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下
    2023-04-04
  • Ubuntu16.04下配置VScode的C/C++開發(fā)環(huán)境

    Ubuntu16.04下配置VScode的C/C++開發(fā)環(huán)境

    這篇文章主要介紹了Ubuntu16.04下配置VScode的C/C++開發(fā)環(huán)境的教程,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-03-03

最新評論

德阳市| 郁南县| 马尔康县| 柳州市| 双鸭山市| 三亚市| 普定县| 泾川县| 江华| 竹北市| 库尔勒市| 张北县| 沭阳县| 合阳县| 孟连| 白玉县| 民权县| 论坛| 偏关县| 马关县| 乐山市| 五河县| 玉树县| 尉氏县| 高雄县| 诏安县| 巴楚县| 梨树县| 三亚市| 深水埗区| 竹溪县| 晋州市| 泉州市| 肃南| 长春市| 邻水| 温宿县| 顺平县| 福建省| 宜城市| 麻江县|