基本
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);
}
};
由於互相參照的部分是樣板,因此不論如何調換 Bar
或 Foo
的宣告順序或使用前置宣告都沒辦法解決這個問題;我後來是採用將 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.
留言
張貼留言