跳到主要內容

C++ 明確樣板具現化 (C++ Explicit Template Instantiation)

C++ 明確樣板具現化 (C++ Explicit Template Instantiation)

基本

C++ 裡面,樣板在編譯期時才會被具現化 (instantiated),例如

template<typename T>
struct Foo {
  Foo(T t) : t_(t) {}
  T t_;
};
// Foo<int> 在下一行被具現化
void g() { Foo<int> f(123); }

明確具現化,則是站在設計樣板函示庫者的角度,提供用戶具現化樣板,將明確具現化的類別放在標頭檔如下

// --- foo.h 標頭檔內容
template<typename T>
struct Foo { /* ... */ };
// 明確具現化宣告,引入此標頭檔的編譯單元不會嘗試具現化 Foo<int> 及 Foo<double>
extern template struct Foo<int>;
extern template struct Foo<double>;

並將具現化類別定義放在實作檔案

// --- foo.cc
#include "foo.h"
// 明確具現化定義
template struct Foo<int>;
template struct Foo<double>;

這與一般分割宣告與定義相同,差別只在多了樣板的語法。這個做法用意在於提供常用的具現化樣板類別,這樣多個轉譯單元 (translation unit) 在編譯時不必每遇到 Foo<int> 都具現化一次,從而減少編譯時間。

動態連結 (Dynamic Linking)

跟一般的動態函示庫相同,當樣板類別被明確具現化後,也需要被匯出 (export) 才能夠透過動態繫結使用。下面是一份常見的匯出輔助標頭檔:

// export.h
#pragma once

#if defined(COMPONENT_BUILD)
#if defined(WIN32)
#if defined(FOO_IMPLEMENTATION)
#define FOO_EXPORT __declspec(dllexport)
#else
#define FOO_EXPORT __declspec(dllimport)
#endif  // defined(FOO_IMPLEMENTATION)
#else  // defined(WIN32)
#if defined(FOO_IMPLEMENTATION)
#define FOO_EXPORT __attribute__((visibility("default")))
#else
#define FOO_EXPORT
#endif  // defined(FOO_IMPLEMENTATION)
#endif
#else  // defined(COMPONENT_BUILD)
#define FOO_EXPORT
#endif

其中

  • COMPONENT_BUILD 在編譯動態函示庫時會被定義,靜態則不會
  • WIN32 在 Windows 平台上會被定義,反之不會
  • FOO_IMPLEMENTATION 在編譯 foo.cc 的轉譯單元時需要被定義,通常是由編譯腳本 make, ninja 或是 cmake, gn 等腳本產生器控制
  • FOO_EXPORT 在編譯 foo.cc 的轉譯單元時會被定義成匯出屬性 __declspec(dllexport),其他轉譯單元則會被定義成 __declspec(dllimport),這些屬性會由連結器 (linker) 處理
// --- foo.h
#include "export.h"
template<typename T>
struct Foo {
  Foo();
  ~Foo();
  T t_;
};
#ifndef FOO_IMPLEMENTATION
extern template struct FOO_EXPORT Foo<int>;
extern template struct FOO_EXPORT Foo<double>;
#endif

// --- foo.cc
#include "foo.h"
// Foo 的非樣板成員函式不能 inline 定義在 foo.h 裡面
template<typename T> Foo<T>::Foo() = default;
template<typename T> Foo<T>::~Foo() = default;
// 明確具現化定義
template struct FOO_EXPORT Foo<int>;
template struct FOO_EXPORT Foo<double>;

另外當有命名空間 (namespace) 存在時,連結器缺乏命名解析的能力,因此 Foo<int> 須採用全資格 (fully qualified) 名稱,避免連結時找不到;例如

// --- foo.h
namespace N {
template<typename T>
struct Foo{ /* ... */ };
}  // namespace N

// --- foo.cc
#include "foo.h"
#include "export.h"
template struct FOO_EXPORT N::Foo<int>;
template struct FOO_EXPORT N::Foo<double>;

解決樣板互相參照情境

先考慮這樣的情境─樣板類別與其他型別互相引用而產生編譯錯誤:

struct Foo {
  template<typename T>
  T FooFunction(Bar* bar, T t) {
    return bar->GetValue() * t;
  }
};
struct Bar {
  int GetValue() { return 42; }
  
  template<typename T>
  T BarFuncion(T t) {
    Foo foo;
    return foo.FooFunction(this, t);
  }
};

由於互相參照的部分是樣板,因此不論如何調換 BarFoo 的宣告順序或使用前置宣告都沒辦法解決這個問題;我後來是採用將 Foo 改成樣板類別 Foo<Bar>,讓 Bar 變成了樣板參數,這樣當 Foo<Bar> 樣板具現化的時候 Bar 的定義會是已知。

template<typename BAR>
struct Foo {
  template<typename T>
  T FooFunction(BAR* bar, T t) {
    return bar->GetValue() * t;
  }
};
struct Bar { /* ... */ };

這個情境下,Foo 完全是為了解決互相參照而改成樣板類別,因此提供 Foo<Bar> 的明確具現化是非常合理的:

// --- foo.h
#include "export.h"
template<typename BAR>
struct Foo { /* ... */ };
struct Bar; // 前置宣告
#ifndef FOO_IMPLEMENTATION
extern template struct FOO_EXPORT Foo<Bar>;
#endif

// --- foo.cc
#include "bar.h"
template<typename T> Foo<T>::Foo() = default;
template<typename T> Foo<T>::~Foo() = default;
template struct FOO_EXPORT Foo<Bar>;

// --- bar.h
#include "foo.h"
struct Bar { /* ... */ };

Written with StackEdit.

留言

這個網誌中的熱門文章

得利油漆色卡編碼方式

得利油漆色卡編碼方式 類似 Munsell 色彩系統 ,編碼方式為 HUE LRV/CHROMA 例如 10GY 61/449 ( 色卡 ) 編碼數值 描述 10GY hue ,色輪上從 Y(ellow) 到 G(reen) 區分為 0 ~ 99 ,數值越小越靠近 Y,越大越靠近 G 61 LRV (Light Reflectance Value) 塗料反射光源的比率,數值從 0% ~ 100% ,越高越亮,反之越暗,也可理解為明度 449 chroma 可理解為彩度,數值沒有上限,越高顏色純度 (濃度) 越高 取決於測量儀器,對應至 RGB 並不保證視覺感受相同。 參考資料: 色卡對照網站 e-paint.co.uk Written with StackEdit .

UTF8 與 Unicode 的轉換 (C++)

UTF8 與 Unicode 的轉換 (C++) 先釐清一下這兩者的性質 Unicode: 為世界上所有的文字系統制訂的標準,基本上就是給每個字(letter)一個編號 UTF-8: 為 unicode 的編號制定一個數位編碼方法 UTF-8 是一個長度介於 1~6 byte 的編碼,將 unicode 編號 (code point) 分為六個區間如下表 1 Bits First code point Last code point Bytes Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 Byte 6 7 U+0000 U+007F 1 0xxxxxxx 11 U+0080 U+07FF 2 110xxxxx 10xxxxxx 16 U+0800 U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx 21 U+10000 U+1FFFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 26 U+200000 U+3FFFFFF 5 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 31 U+4000000 U+7FFFFFFF 6 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 觀察上面的表應該可以發現 除了 7 bits 的區間外,第一個 byte 開頭連續 1 的個數就是長度,例如 110XXXXX 就是 2 byte 長,而 1110xxxx 就是 3 byte 除了第一個 byte 外,之後的 byte 前兩個 bit 一定是 10 開頭,這樣的好處在於確立了編碼的 self-synchronizeing,意即當編碼為多個 byte 時,任取一個 byte 無法正常解碼。 Note 第一點中的例外 (7 bits) 是為了與 ASCII 的相容性,而第二點會影響到 code point 至 UTF-8 的轉換。 為了與 UTF-16 的相容性,在 R...

C++17 新功能 try_emplace

C++17 新功能 try_emplace 回顧 emplace 大家的好朋友 Standard Template Library (STL) 容器提供如 push_back , insert 等介面,讓我們塞東西進去; C++11 之後,新增了 emplace 系列的介面,如 std::vector::emplace_back , std::map::emplace 等,差異在於 emplace 是在容器內 in-place 直接建構新元素,而不像 push_back 在傳遞參數前建構,下面用實例來說明: struct Value { // ctor1 Value ( int size ) : array ( new char [ size ] ) , size ( size ) { printf ( "ctor1: %d\n" , size ) ; } // ctor2 Value ( const Value & v ) : array ( new char [ v . size ] ) , size ( v . size ) { printf ( "ctor2: %d\n" , size ) ; memcpy ( array . get ( ) , v . array . get ( ) , size ) ; } private : std :: unique_ptr < char [ ] > array ; int size = 0 ; } ; struct Value 定義了自訂建構子 (ctor1),以指定大小 size 配置陣列,複製建構子 (ctor2) 則會配置與來源相同大小及內容的陣列,為了方便觀察加了一些 printf 。當我們如下使用 std::vector::push_back 時 std :: vector < Value > v ; v . push_back ( Value ( 2048 ) ) ; 首先 Value 會先呼叫 ctor1,傳給 push_ba...