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

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

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

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

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

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

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

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

2. 值語義的誘惑與代價(jià)

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

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

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

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

3. 指針語義的回退

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

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

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

  • 拷貝構(gòu)造函數(shù)(Copy Constructor)
  • 拷貝賦值運(yùn)算符(Copy Assignment Operator)
  • 移動構(gòu)造函數(shù)(Move Constructor)
  • 移動賦值運(yùn)算符(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 呢?其實(shí)也可以,不過在現(xiàn)代 C++ 中,更推薦使用 std::unique_ptr 。std::shared_ptr 是用來共享資源的所有權(quán),會對引用資源進(jìn)行計(jì)數(shù),但是有可能會造成相互循環(huán)引用造成不能釋放資源的問題;而std::unique_ptr 則表示獨(dú)占資源的所有權(quán),不僅開銷更低(無引用計(jì)數(shù)),也更加安全(只能通過 std::move 轉(zhuǎn)移所有權(quán) )。

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

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

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

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

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

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

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

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

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

那么要怎么進(jìn)行優(yōu)化呢?很簡單,我們可以實(shí)現(xiàn)一個(gè)名為 Impl 的類中類 ,使用std::unique_ptr進(jìn)行管理。Impl 是實(shí)現(xiàn)在 cpp 中的,可以將一切實(shí)現(xiàn)的細(xì)節(jié),比說私有函數(shù)和數(shù)據(jù)成員,都放在這個(gè) 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; // 此時(shí) Impl 完整,安全!

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

6. 沒有銀彈,只有權(quán)衡

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

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

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

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

7. 示例代碼

最后放出筆者自己實(shí)現(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();

    // 假設(shè)輸出是 [batch, seq, dim] 或 [batch, dim]
    // 我們?nèi)∽詈笠粋€(gè)非 -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"};

    // 把三個(gè)輸入張量放進(jìn)數(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(),  // 運(yùn)行選項(xiàng)(通常 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個(gè)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;  // 此時(shí) Impl 已定義,可安全析構(gòu)

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

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

相關(guān)文章

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

    c語言中unsigned修飾符的使用

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

    C語言每日練習(xí)之字符串反轉(zhuǎn)

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

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

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

    C++ move 的作用詳解及陷阱最佳實(shí)踐

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

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

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

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

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

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

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

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

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

    C語言輸入一個(gè)數(shù)判斷是否為素?cái)?shù)的多種方法

    素?cái)?shù)是只能被1和它自己本身整除,不能被其他自然數(shù)整除的大于1的正整數(shù),下面這篇文章主要給大家介紹了關(guān)于C語言輸入一個(gè)數(shù)判斷是否為素?cái)?shù)的多種方法,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    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)境的教程,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-03-03

最新評論

泰宁县| 山丹县| 江安县| 中宁县| 达州市| 桂林市| 疏勒县| 浮梁县| 沁源县| 呼伦贝尔市| 西吉县| 青浦区| 泊头市| 新乐市| 东宁县| 镇远县| 务川| 铜川市| 南阳市| 潞西市| 麻城市| 兴化市| 静宁县| 余江县| 阳山县| 兰坪| 永清县| 津南区| 涿鹿县| 东丽区| 临清市| 巨鹿县| 临清市| 德州市| 芒康县| 稷山县| 昭通市| 邹平县| 封丘县| 漾濞| 嵊泗县|