深入淺出之STL源碼分析6_模版編譯問題

1.模版編譯原理

當我們在代碼中使用了一個模板,觸發了一個實例化過程時,編譯器就會用模板的實參(Arguments)去替換(Substitute)模板的形參(Parameters),生成對應的代碼。同時,編譯器會根據一定規則選擇一個位置,將生成的代碼插入到這個位置中,這個位置被稱為 POI(point of instantiation)。由于要做替換才能生成具體的代碼,因此 C++ 要求模板的定義對它的 POI 一定要是可見的。換句話說,在同一個翻譯單元(Translation Unit)中,編譯器一定要能看到模板的定義,才能對其進行替換,完成實例化。

2.包含模型

因此最常見的做法是,我們會將模板定義在頭文件中,然后再源文件中?#include?頭文件來獲取該模板的定義。這就是模板編程中的包含模型(Inclusion Model)。

所以我們一般情況下,對于模版會把定義放在頭文件中。但是這樣操作會帶來缺點。這個也對應著顯示實例化的優點。

現在的一些 C++ 庫,整個項目中就只有頭文件,沒有源文件,庫的邏輯全部由模板實現在頭文件中。而且這種做法似乎越來越流行,在 GitHub 和 boost 中能看到很多很多。我想原因一個是 C++ 缺乏一個官方的 package manager,這樣發布的軟件包更易使用(include就行了);另一個就是模板實例化的這種要求,使得包含模型成為泛型編程中組織代碼最容易的方式。

但包含模型也有自身的問題。在一個翻譯單元(Translation Unit)中,同一個模板實例只會被實例化一次。也就是對同一個模板傳入相同的實參,編譯器會先檢查是否已實例化過,如果是則使用之前實例化的結果。但在不同的翻譯單元中,相同實參的模板會被實例化多次,從而產生多個相同的類型、函數和變量

這帶來兩個問題:

2.1 鏈接時的重定義問題

如果不加以處理,這些相同的實體會被鏈接器認為是重定義的符號,這違反了ODR(One Definition Rule)。對這個問題的主流解決方案是為模板實例化生成的實體添加特殊標記,鏈接器在鏈接時對有標記的符號做特殊處理。例如在 GNU 體系下,模板實例化生成的符號都被標記為弱符號(Weak Symbol)。不需要我們參與,連接器已經為我們解決了這個問題。

對于普通的函數和類,連接器是不會處理的。會報重定義的錯誤。

同時這個因為每一個編譯單元都有實例化,會帶來代碼膨脹。

2.2 編譯時長的問題

同一個模板傳入相同實參在不同的編譯單元下被實例化了多次,這是不必要的,浪費了編譯的時間。

3.分離模型

就是將聲明放在頭文件,實現放在.cpp中,并對其進行顯示實例化,為什么要進行顯示實例化,我在后面會詳細的介紹。

顯示實例化有如下的優點:

1. 降低編譯器構建的時間

2.降低代碼膨脹

3.針對發布lib,可以隱藏頭文件

當然缺點也很明顯,你要用到哪個模版類型,你提前事先都得很清楚,并且隨著代碼的累加,你都要動態的維護。

4.驗證前面說的結論

1.如果在翻譯單元的編譯期間,能夠看到模版的定義,就會在當前單元進行實例化。

也就是這個是一個多余的動作,就是能看見就順便實例化了,調用函數正常。

2.如果在當前翻譯單元看不到模版的定義,則只會調用這個函數,也就是認為聲明在這個.h文件中,定義不在,鏈接的時候再去找。

3.正常的編譯期就會生成對成員函數的調用,只是能看到模版的定義時,在本翻譯單元的時候,才會進行實例化。如果在本翻譯單元沒有定義,則會在鏈接階段再去找定義。

4.只要是實例化一定是在編譯階段,只是在你這個單元是否可見。

以上說的這幾點,主要是針對包含模型和分離模型的對比來說的。

下面我們看下具體的例子。

我們先來解釋第一句話:

//1.如果在翻譯單元的編譯期間,能夠看到模版的定義,就會在當前單元進行實例化。
//也就是這個是一個多余的動作,就是能看見就順便實例化了,調用函數正常。
//我們定義寫一個main.cpp,再寫一個function_template.h,然后在function_template.h中寫一個實現的函數模版
// main.cpp
#include <iostream>
//#include "zhang.h"
#include "function_template.h"
int main() {int aa = 12;//Zhang temp;int num = system_latency_get_hardware_time(aa);std::cout<<num<<std::endl;return 0;
}
//function_template.h
template <typename Message>
int system_latency_get_hardware_time(const Message& message) {int a = 16;int b = 17;return a+b;
}
// 編譯指令如下:
g++ -std=c++17 main.cpp   -o main
// 編譯通過,執行./main
33
// 我們再來看下,分步操作會怎么樣,我先生成匯編代碼.
g++ -std=c++17 -S main.cpp -o main.s

查看對應的匯編代碼,發現在匯編里已經出現了 system_latency_get_hardware_time

的定義

//我們修改 function_template.h的代碼
// function_template.h
template <typename Message>
int system_latency_get_hardware_time(const Message& message);
// 然后再進行匯編操作
g++ -std=c++17 -S main.cpp -o main.s

這個時候我們匯編不會報錯,正如我們第二點所說.

2.如果在當前翻譯單元看不到模版的定義,則只會調用這個函數,也就是認為聲明在這個.h文件中,定義不在,鏈接的時候再去找。

關于這個函數搜索只能找到這一條數據。

而我們現在繼續向下,鏈接會怎么樣,應該會報鏈接錯誤,我們試試.

// 為了少打幾個指令,直接進行一步
g++ -std=c++17 main.cpp   -o main
這個時候,在鏈接的時候,就會報錯g++ -std=c++17 main.cpp   -o main
/tmp/ccgzva3Q.o: In function `main':
main.cpp:(.text+0x26): undefined reference to `int system_latency_get_hardware_time<int>(int const&)'
collect2: error: ld returned 1 exit status

這個錯誤,我們是可以接受的,因為我們確實沒有定義.

那我們想把,模版的定義,放到.cpp中可以嗎?

// 在剛才的基礎上
// function_template.h
template <typename Message>
int system_latency_get_hardware_time(const Message& message);
// add function_template.cpp
#include "function_template.h"
template <typename Message>
int system_latency_get_hardware_time(const Message& message) {int a = 16;int b = 17;return a+b;
}
// 然后我在編譯代碼的時候,鏈接下function_template.cpp是不是就可以了呢?
// g++ -std=c++17 main.cpp  function_template.cpp -o main
/tmp/ccsWDsr1.o: In function `main':
main.cpp:(.text+0x26): undefined reference to `int system_latency_get_hardware_time<int>(int const&)'
collect2: error: ld returned 1 exit status

我們發現還是不行,這個其實很好理解,因為在 另外一個編譯單元(一個cpp文件就是一個編譯單元), function_template.cpp中,這個不會生成這個函數,因為模版沒有被實例話,那么方法來了,我們來進行下顯示的實例話。

// function_template.cpp
#include "function_template.h"
template <typename Message>
int system_latency_get_hardware_time(const Message& message) {int a = 16;int b = 17;return a+b;
}
// 顯示實例話  這個必須放在定義的后面,在.h和.cpp里無所謂.
template int system_latency_get_hardware_time<int> (const int&);

再進行編譯,g++ -std=c++17 main.cpp function_template.cpp -o main

我們發現可以編譯通過了,說明在 function_template.cpp中,會生成這個函數,那么我們沒有給函數體,函數體是誰呢?很顯然是是使用 primary template的函數體,因為執行程序的輸出是 33.

到現在為止,1,2,3,4 點我都解釋清楚了。

這里要主要一個細節,就是類模版的全特化和函數模版的全特化還有些不同,類模版如果全特化以后,還需要進行實例化才能生成代碼,但是函數模版的全特化后,不需要實例化了,直接可以生成代碼,具體可以參考我的下一篇博文?深入淺出之STL源碼分析5_模版實例化與全特化-CSDN博客

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/82597.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/82597.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/82597.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

無人甘蔗小車履帶式底盤行走系統的研究

1.1 研究背景與意義 1.1.1 研究背景 甘蔗作為全球最重要的糖料作物之一&#xff0c;在農業經濟領域占據著舉足輕重的地位。我國是甘蔗的主要種植國家&#xff0c;尤其是廣西、廣東、云南等地&#xff0c;甘蔗種植面積廣泛&#xff0c;是當地農業經濟的重要支柱產業。甘蔗不僅…

LVGL(lv_slider滑動條)

文章目錄 一、lv_slider 是什么&#xff1f;二、創建一個滑塊設置滑塊的范圍和初始值 三、響應滑塊事件四、設置樣式示例&#xff1a;更改滑塊顏色和滑塊按鈕樣式 五、縱向滑塊&#xff08;垂直方向&#xff09;六、雙滑塊模式&#xff08;范圍選擇&#xff09;七、獲取滑塊的值…

每日算法-250511

每日算法 - 250511 記錄一下今天刷的幾道LeetCode題目&#xff0c;主要是關于貪心算法和數組處理。 1221. 分割平衡字符串 題目 思路 貪心 解題過程 我們可以遍歷一次字符串&#xff0c;維護一個計數器 balance。當遇到字符 L 時&#xff0c;balance 增加&#xff1b;當遇…

Keepalived + LVS + Nginx 實現高可用 + 負載均衡

目錄 Keepalived Keepalived 是什么&#xff08;高可用&#xff09; 安裝 Keepalived LVS LVS 是什么&#xff08;負載均衡&#xff09; 安裝 LVS Keepalived LVS Nginx 實現 高可用 負載均衡 Keepalived Keepalived 是什么&#xff08;高可用&#xff09; Keepaliv…

【雜談】-DeepSeek-GRM:讓AI更高效、更普及的先進技術

DeepSeek-GRM&#xff1a;讓AI更高效、更普及的先進技術 文章目錄 DeepSeek-GRM&#xff1a;讓AI更高效、更普及的先進技術1、DeepSeek-GRM&#xff1a;先進的AI框架解析2、DeepSeek-GRM&#xff1a;AI開發的變革之力3、DeepSeek-GRM&#xff1a;廣泛的應用前景4、企業自動化解…

【MySQL】頁結構詳解:頁的大小、分類、頭尾信息、數據行、查詢、記錄及數據頁的完整結構

&#x1f4e2;博客主頁&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;博客倉庫&#xff1a;https://gitee.com/JohnKingW/linux_test/tree/master/lesson &#x1f4e2;歡迎點贊 &#x1f44d; 收藏 ?留言 &#x1f4dd; 如有錯誤敬請指正&#xff01; &…

【FreeRTOS】基于G431+Cubemx自用筆記

系列文章目錄 留空 文章目錄 系列文章目錄前言一、從頭開始創建一個FreeRTOS工程1.1 在 "Timebase Source" 中&#xff0c;選擇其他TIM1.2 配置FreeRTOS的參數1. 3 添加任務 二、動態任務的創建/刪除2.1 函數介紹2.1.1 創建動態任務xTaskCreate()2.1.2 創建靜態任務…

LVGL(lv_bar進度條)

文章目錄 一、lv_bar 是什么&#xff1f;二、基本使用創建一個進度條設置進度值 三、條形方向與填充方向四、范圍模式&#xff08;Range&#xff09;五、事件處理&#xff08;可選&#xff09;六、自定義樣式&#xff08;可選&#xff09;七、綜合示例八、配合 lv_timer 或外部…

AI對話小技巧

角色設定&#xff1a;擅于使用 System 給 GPT 設定角色和任務&#xff0c;如“哲學大師"指令注入&#xff1a;在 System 中注入常駐任務指令&#xff0c;如“主題創作"問題拆解&#xff1a;將復雜問題拆解成的子問題&#xff0c;分步驟執行&#xff0c;如&#xff1a…

C++ 核心基礎:數字、數組、字符串、指針與引用詳解

C++ 核心基礎:數字、數組、字符串、指針與引用詳解 1. C++ 基礎語法1.1 標識符與保留字1.2 數據類型概述1.3 基本輸入輸出2.1 基本整數類型(int、short、long、long long)2.2 無符號整數類型(unsigned int、unsigned short、unsigned long、unsigned long long)2.3 整數類…

HarmonyOS運動開發:如何集成百度地圖SDK、運動跟隨與運動公里數記錄

前言 在開發運動類應用時&#xff0c;集成地圖功能以及實時記錄運動軌跡和公里數是核心需求之一。本文將詳細介紹如何在 HarmonyOS 應用中集成百度地圖 SDK&#xff0c;實現運動跟隨以及運動公里數的記錄。 一、集成百度地圖 SDK 1.引入依賴 首先&#xff0c;需要在項目的文…

如何理解k8s中的controller

一、基本概念 在k8s中&#xff0c;Controller&#xff08;控制器&#xff09;是核心組件之一&#xff0c;其負責維護集群狀態并確保集群內的實際狀態與期望狀態一致的一類組件。控制器通過觀察集群的當前狀態并將其與用戶定義的期望狀態進行對比&#xff0c;做出相應的調整來實…

《Go小技巧易錯點100例》第三十二篇

本期分享&#xff1a; 1.sync.Map的原理和使用方式 2.實現有序的Map sync.Map的原理和使用方式 sync.Map的底層結構是通過讀寫分離和無鎖讀設計實現高并發安全&#xff1a; 1&#xff09;雙存儲結構&#xff1a; 包含原子化的 read&#xff08;只讀緩存&#xff0c;無鎖快…

【MySQL】行結構詳解:InnoDb支持格式、如何存儲、頭信息區域、Null列表、變長字段以及與其他格式的對比

&#x1f4e2;博客主頁&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;博客倉庫&#xff1a;https://gitee.com/JohnKingW/linux_test/tree/master/lesson &#x1f4e2;歡迎點贊 &#x1f44d; 收藏 ?留言 &#x1f4dd; 如有錯誤敬請指正&#xff01; &…

LabVIEW多通道并行數據存儲系統

在工業自動化監測、航空航天測試、生物醫學信號采集等領域&#xff0c;常常需要對多個傳感器通道的數據進行同步采集&#xff0c;并根據后續分析需求以不同采樣率保存特定通道組合。傳統單線程數據存儲方案難以滿足實時性和資源利用效率的要求&#xff0c;因此設計一個高效的多…

【Linux系列】bash_profile 與 zshrc 的編輯與加載

&#x1f49d;&#x1f49d;&#x1f49d;歡迎來到我的博客&#xff0c;很高興能夠在這里和您見面&#xff01;希望您在這里可以感受到一份輕松愉快的氛圍&#xff0c;不僅可以獲得有趣的內容和知識&#xff0c;也可以暢所欲言、分享您的想法和見解。 推薦:kwan 的首頁,持續學…

針對Mkdocs部署到Githubpages加速訪問速度的一些心得

加速網站訪問的一些心得 在使用 MkDocs 構建網站時&#xff0c;為了提高訪問速度&#xff0c;我們可以采取以下一些措施&#xff1a; 1. 優化圖片 使用合適的圖片格式&#xff0c;如 WebP、JPEG2000 等&#xff0c;減少圖片文件大小&#xff0c;從而加快加載速度。 可以使用…

Mysql中切割字符串作為in的查詢條件

問題&#xff1a;需要將一個字符串切割成數組作為in的查詢條件&#xff0c;如&#xff1a; select * from table_1 where name in (select slit(names) from table_2 where id 3); names 返回的格式是’name1,name2,name3…,需要將name按照逗號切割作為in的查詢條件&#xff1b…

云計算中的虛擬化:成本節省、可擴展性與災難恢復的完美結合

云計算中虛擬化的 4 大優勢 1. 成本效益 從本質上講&#xff0c;虛擬化最大限度地減少了硬件蔓延。團隊可以將多個虛擬機整合到單個物理主機上&#xff0c;而不是為每個工作負載部署單獨的服務器。這大大減少了前期硬件投資和持續維護。 結果如何&#xff1f;更低的功耗、更低…

Linux : 多線程【線程概念】

Linux &#xff1a; 多線程【線程概念】 &#xff08;一&#xff09;線程概念線程是什么用戶層的線程linux中PID與LWP的關系 (二) 進程地址空間頁表(三) 線程總結線程的優點線程的缺點線程異常線程用途 &#xff08;一&#xff09;線程概念 線程是什么 在一個程序里的一個執行…