C++11 外部模板


【1】引入外部模板為了解決什么問題?

“外部模板”是C++11中一個關於模板性能上的改進。實際上,“外部”(extern)這個概念早在C的時候已經就有了。

常見的情況,在一個文件a.c中定義了一個變量int i,而在另外一個文件b.c中想使用它,這個時候就會在沒有定義變量i的b.c文件中做一個外部變量的聲明。比如:

// 聲明在b.c文件中
extern int i;

這樣做的好處是,在分別編譯了a.c和b.c之后,其生成的目標文件a.o和b.o中只有i這個符號(可簡單理解為變量)的一份定義。

具體地,a.o中的i是實在存在於a.o目標文件的數據區中的數據,而在b.o中,只是記錄了i符號會引用其他目標文件中數據區中的名為i的數據。

這樣一來,在鏈接器(通常由編譯器代為調用)將a.o和b.o鏈接成單個可執行文件(或者庫文件)c的時候,c文件的數據區也只會有一個i的數據(供a.c和b.c的代碼共享)。

而如果b.c中我們聲明int i的時候不加上extern的話,那么i就會實實在在地既存在於a.o的數據區中,也存在於b.o的數據區中。

那么鏈接器在鏈接a.o和b.o的時候,就會報告錯誤,因為無法決定相同的符號是否需要合並。

而對於函數模板來說,現在我們遇到的幾乎是一模一樣的問題。

不同的是,發生問題的不是變量(數據),而是函數(代碼)。這樣的困境是由於模板的實例化帶來的。

注意下文主要以函數模板為例,因為其只涉及代碼,講解起來比較直觀。如果是類模板,則有可能涉及數據,不過其原理都是類似的。

比如,在一個test.h的文件中聲明了如下一個函數模板:

// test.h
template <typename T> 
void fun(T) 
{ }

在第一個test1.cpp文件中,為了利用函數模板,定義了以下代碼:

// test1.cpp
#include "test.h" 
void test1() 
{ 
    fun(1); 
}

而在另一個test2.cpp文件中,為了利用函數模板,定義了以下代碼:

// test2.cpp
#include "test.h" 
void test2()
{
    fun(2);
}

由於兩個源代碼使用的函數模板的參數類型一致:

所以在編譯test1.cpp的時候,編譯器實例化出了函數fun<int>(int),而當編譯test2.cpp的時候,編譯器又再一次實例化出了函數fun<int>(int)。

那么可以想象,在test1.o目標文件和test2.o目標文件中,會有兩份一模一樣的函數fun<int>(int)代碼。代碼重復和數據重復不同。

數據重復,編譯器往往無法分辨是否是要共享的數據;而代碼重復,為了節省空間,保留其中之一就可以了(只要代碼完全相同)。

事實上,大部分鏈接器也是這樣做的。在鏈接的時候,鏈接器通過一些編譯器輔助的手段將重復的模板函數代碼fun<int>(int)刪除掉,只保留了單個副本。

這樣一來,就解決了模板實例化時產生的代碼冗余問題。可以看看下圖中的模板函數的編譯與鏈接的過程示意:

很明顯,對於源代碼中出現的每一處模板實例化,編譯器都需要去做實例化的工作;而在鏈接時,鏈接器還需要移除重復的實例化代碼。

很明顯,這樣的工作太過冗余,而在廣泛使用模板的項目中,由於編譯器會產生大量冗余代碼,會極大地增加編譯器的編譯時間和鏈接時間。

解決這個問題的方法基本跟變量共享的思路是一樣的,就是使用“外部的”模板。

【2】顯式實例化與外部模板的聲明

外部模板的使用實際依賴於C++98中一個已有的特性,即顯式實例化(Explicit Instantiation)。

顯式實例化的語法很簡單,比如對於以下模板:

template <typename T>
void fun(T) {}

只需要聲明:

template void fun<int>(int);

這就可以使編譯器在本編譯單元中實例化出一個fun<int>(int)版本的函數(這種做法也被稱為強制實例化)。

而在C++11標准中,又加入了外部模板(Extern Template)的聲明。

語法上,外部模板的聲明跟顯式的實例化差不多,只是多了一個關鍵字extern。對於上面的例子,可以通過:

extern template void fun<int>(int);

這樣的語法完成一個外部模板的聲明。那么回到一開始我們的例子,來修改一下我們的代碼。

首先,在test1.cpp做顯式地實例化:

// test1.cpp
#include "test.h"
template void fun<int>(int); // 顯式地實例化 
void test1()
{ 
    fun(1);
}

接下來,在test2.cpp中做外部模板的聲明:

// test2.cpp
#include "test.h"
extern template void fun<int>(int); // 外部模板的聲明
void test2()
{
    fun(2);
}

這樣一來,在test2.o中不會再生成fun<int>(int)的實例代碼。整個模板的實例化流程如下圖所示:

 

可以看到,由於test2.o不再包含fun<int>(int)的實例,因此鏈接器的工作很輕松,基本跟外部變量的做法是一樣的,即只需要保證讓test1.cpp和test2.cpp共享一份代碼位置即可。

而同時,編譯器也不用每次都產生一份fun<int>(int)的代碼,所以可以減少編譯時間。

這里也可以把外部模板聲明放在頭文件中,這樣所有包含test.h的頭文件就可以共享這個外部模板聲明了。

這一點跟使用外部變量聲明是完全一致的。

【3】外部模板的應用

應用外部模板注意問題:

(1)如果外部模板聲明出現於某個編譯單元中,那么與之對應的顯示實例化必須出現於另一個編譯單元中或者同一個編譯單元的后續代碼中。

(2)外部模板聲明不能用於一個靜態函數(即文件域函數),但可以用於類靜態成員函數(顯而易見,因為靜態函數沒有外部鏈接屬性,不可能在本編譯單元之外出現)。

應用示例:

[1] extern_template.h 外部模板頭文件

 1 // extern_template.h
 2 
 3 __pragma(once)
 4 
 5 #include <iostream>
 6 
 7 // 模板
 8 
 9 //
10 template <typename T>
11 class A {};
12 
13 // 靜態成員方法
14 template <typename T>
15 class B 
16 {
17 public:
18     static void sm_f(T t)
19     {
20         std::cout << "B::sm_f :: " << t << std::endl;
21     }
22 };
23 
24 // 靜態普通函數
25 template <typename T>
26 static void s_f(T t) 
27 {
28     std::cout << "static s_f :: " << t << std::endl;
29 };
30 
31 // 普通函數
32 template <typename T>
33 void func(T t)
34 {
35     std::cout << "func :: " << t << std::endl;
36 };

 [2] extern_template.cpp 實例化

 1 // extern_template.cpp
 2 
 3 #include "extern_template.h"
 4 
 5 // 實例化 
 6 
 7 void test_f1()
 8 {
 9     A<int> a;
10     s_f(1);
11     std::cout << "static general func" << std::endl;
12 }
13 
14 void test_f2()
15 {
16     func(1);
17     std::cout << "general func" << std::endl;
18 }
19 
20 void test_f3()
21 {
22     B<int>::sm_f(1);
23     std::cout << "static member func" << std::endl;
24 }

 [3] main.cpp 外部模板聲明與應用

 1 // main.cpp
 2 
 3 #include <iostream>
 4 #include "extern_template.h"
 5 
 6 extern template class A<int>;            //
 7 extern template void B<int>::sm_f(int);  // 靜態成員函數
 8 
 9 #if 0
10 extern template void s_f<int>(int);      // error C2129 靜態函數“void s_f<int>(int)”已聲明但未定義
11 #endif
12 
13 extern template void func<int>(int);     // 普通函數
14 
15 void test_f() 
16 {
17     func(1);
18     std::cout << "test_f" << std::endl;
19 }
20 
21 int main()
22 {
23     s_f(1);
24     test_f();
25     B<int>::sm_f(1);
26 
27     getchar();
28     return 0;
29 }
30 
31 /*
32 static s_f :: 1
33 func :: 1
34 test_f
35 B::sm_f t :: 1
36 */

如上可見:靜態函數沒有外部鏈接屬性,不可能在本編譯單元之外出現。

 

good good study, day day up.

順序 選擇 循環 總結


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM