為什麼你的static_assert不能按預期的工作?

来源:https://www.cnblogs.com/apocelipes/archive/2022/11/21/16910089.html
-Advertisement-
Play Games

static_assert是c++11添加的新語法,它可以使我們在編譯期間檢測一些斷言條件是否為真,如果不滿足條件將會產生一條編譯錯誤信息。 使用靜態斷言可以提前暴露許多問題到編譯階段,極大的方便了我們對代碼的排錯,提前將一些bug扼殺在搖籃里。 然而有時候靜態斷言並不能如我們預期的那樣工作,今天就 ...


static_assert是c++11添加的新語法,它可以使我們在編譯期間檢測一些斷言條件是否為真,如果不滿足條件將會產生一條編譯錯誤信息。

使用靜態斷言可以提前暴露許多問題到編譯階段,極大的方便了我們對代碼的排錯,提前將一些bug扼殺在搖籃里。

然而有時候靜態斷言並不能如我們預期的那樣工作,今天就來看看這些“不正常”的情況,我將舉兩個例子,每個都有一定的代表性。

為什麼我的static_assert不工作

基於靜態斷言可以在編譯期觸發,我們希望實現一個模板類,類型參數不能是int,如果違反約定則會給出編譯錯誤信息:

template <typename T>
struct Obj {
    static_assert(!std::is_same_v<T, int>, "T 不能為 int");
    // do sth with a
};

int main() {
    Obj<int> *ptr = nullptr;
}

按照預期,這段代碼應該觸發靜態斷言導致無法編譯,然而實際運行的結果卻是:

g++ --version

g++ (GCC) 12.2.0
Copyright © 2022 Free Software Foundation, Inc.
本程式是自由軟體;請參看源代碼的版權聲明。本軟體沒有任何擔保;包括沒有適銷性和某一專用目的下的適用性擔保。

g++ -std=c++20 -Wall -Wextra error.cpp
error.cpp: 在函數‘int main()’中:
error.cpp:10:15: 警告:unused variable ‘ptr’ [-Wunused-variable]
   10 |     Obj<int> *ptr = nullptr;
      |               ^~~

事實上除了警告我們ptr沒有被使用,程式被正常編譯了。換clang是一樣的結果。也就是說,static_assert根本沒生效。

這不應該啊?我們明明用到了模板,而static_assert作為類的一部分應該也被編譯器檢測到並被觸發才對。

答案就是,static_assert確實沒有被觸發。

我們先來看看模板類中static_assert在什麼時候生效:當需要顯式或者隱式實例化這個模板類的時候,編譯器就會看見這個靜態斷言,然後檢查斷言是否通過。

但我們這裡不是有Obj<int> *ptr嗎,這難道不會觸發實例化嗎?答案在c++的標準里:

Unless a class template specialization has been explicitly instantiated (17.7.2) or explicitly specialized (17.7.3), the class template specialization is implicitly instantiated when the specialization is referenced in a context that requires a completely-defined object type or when the completeness of the class type affects the semantics of the program. -- C++17 standard §17.7.1

意思是說,除了顯式實例化,模板類還會在需要它實例化的上下文里被隱式實例化,重點在於那個a completely-defined object type

這個“完整的對象類型”是什麼呢?很簡單,就是一個編譯器能看到其完整的類型定義的類型,舉個例子:

class A;

class B {
    int i = 0;
};

這裡的B就是完整的,而A是不完全類型,一個更為人熟知的稱法是:class A是類A的前置聲明。

因為我們沒有A的完整定義,所以我們只能聲明A*或者A&類型的變數或者將A作為函數簽名的一部分,但不能A instance或者new A。因為前兩者是對A的引用,本身不需要知道完整的A是什麼樣的,而作為函數簽名的一部分的時候並不涉及生成實際需要A的代碼,因此也可以使用不完全類型。

所以當你定義一個指針或者引用變數,又或者在寫函數或者類方法的簽名時,他們並不關心前面的類型,只要這個類型的“名字”是存在的且合法的就行,在這些地方並不會導致模板的實例化。所以靜態斷言沒有被觸發。

如何修複這個問題?不使用模板類的指針或者引用可以解決大部分問題,把示例里的Obj<int> *ptr = nullptr改成Obj<int> ptr;,立刻就報錯了:

g++ -std=c++20 -Wall -Wextra error.cpp

error.cpp: In instantiation of ‘struct Obj<int>’:
error.cpp:12:14:   required from here
error.cpp:5:25: 錯誤:static assertion failed: T 不能為 int
    5 |     static_assert(!std::is_same_v<T, int>, "T 不能為 int");
      |                    ~~~~~^~~~~~~~~~~~~~~~~
error.cpp:5:25: 附註:‘!(bool)std::is_same_v<int, int>’ evaluates to false
error.cpp: 在函數‘int main()’中:
error.cpp:12:14: 警告:unused variable ‘ptr’ [-Wunused-variable]
   12 |     Obj<int> ptr;
      |              ^~~

如果我就要指針呢?那也別用原始指針,請用智能指針:std::unique_ptr<Obj<int>> ptr;:

g++ -std=c++20 -Wall -Wextra error.cpp

error.cpp: In instantiation of ‘struct Obj<int>’:
/usr/include/c++/12.2.0/bits/unique_ptr.h:93:16:   required from ‘void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = Obj<int>]’
/usr/include/c++/12.2.0/bits/unique_ptr.h:396:17:   required from ‘std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = Obj<int>; _Dp = std::default_delete<Obj<int> >]’
error.cpp:13:28:   required from here
error.cpp:6:25: 錯誤:static assertion failed: T 不能為 int
    6 |     static_assert(!std::is_same_v<T, int>, "T 不能為 int");
      |                    ~~~~~^~~~~~~~~~~~~~~~~
error.cpp:6:25: 附註:‘!(bool)std::is_same_v<int, int>’ evaluates to false

模板參數可以是不完整類型,但這裡智能指針在析構的時候必須要有完整的類型定義,所以同樣觸發了類型斷言。

這裡還有個坑,shared_ptr可以使用不完整類型,但從原始指針構造shared_ptr或者使用它的方法的時候,不接受非完整類型,所以上述代碼用shared_ptr是不行的。

unique_ptr雖然也可以使用不完整類型,但必須在智能指針對象被析構的地方可以看到被銷毀的類型的完整定義,上面的例子正是在這一步的時候需要類型的完整定義,從而觸發隱式實例化,所以觸發了靜態斷言。

如果我一定要用引用呢?通常這沒有問題,因為引用需要綁定到一個對象,在函數參數里的話雖然沒顯式綁定也很可能在函數代碼里使用了具體類型的某些方法,這些都需要完整的類型定義,從而觸發斷言。如果是引用作為類的成員,則必須提供一個構造函數來保證初始化這些引用,所以也沒有問題。如果你真的遇到問題了,也可以實現現代c++推崇的值語義和移動語義;否則,你應該思考下自己的設計是否真的合理了。

為什麼我的static_assert意外生效了

如果代碼里沒有實例化模板類的操作(包括顯式和隱式)編譯器就不會主動去生成模板的實例,是不是可以利用這點來屏蔽某些不需要的模板重載被觸發呢?

看個例子:

template <typename T>
struct Wrapper {
    // 只接受std::function
    static_assert(0, "T must be a std::function");
};

template <typename T, typename... U>
struct Wrapper<std::function<T(U...)>> {
    // do sth
};

int f(int i) {
    return i*i;
}

int main() {
    std::function<int(int)> func{f};
    Wrapper<decltype(func)> w;
}

我們的Wrapper包裝類只接受std::function,其他的類型不能正常工作,在c++20的concept出來之前我們只能用元編程的做法來實現類似的功能。上面的代碼與SFINAE手法相比既簡單有好理解。

但當我們編譯程式:

g++ -std=c++20 -Wall -Wextra error.cpp

error.cpp:6:19: 錯誤:static assertion failed: T must be a std::function
    6 |     static_assert(0, "T must be a std::function");
      |                   ^
error.cpp: 在函數‘int main()’中:
error.cpp:20:29: 警告:unused variable ‘w’ [-Wunused-variable]
   20 |     Wrapper<decltype(func)> w;
      |                             ^

靜態斷言竟然被觸發了?明明我們的代碼里沒有實例化會報錯的那個模板的地方啊。

這時候看代碼是沒什麼用的,需要看標準怎麼說的:

If no valid specialization can be generated for a template definition, and that template is not instantiated, the template definition is ill-formed, no diagnostic required.

如果模板沒有被使用,也沒有任何針對它的特化或部分特化,且模板內部的代碼有錯誤,編譯器並不需要給出診斷信息。

重點在於“no diagnostic required”,它說不需要,但也沒禁止,所以檢測到模板內部的錯誤並報錯也是正常的,不報錯也是正常的,這個甚至不算undefined behavior

而且我們的靜態斷言里所有的內容都能在編譯期的初步檢查里得到,所以g++和clang++都會產生一條編譯錯誤。

那麼怎麼解決問題呢?我們要確保模板里的代碼至少進行模板類型推導前都是沒法判斷是否合法的。因此除了沒什麼語法上明顯的錯誤,我們需要讓靜態斷言依賴模板參數:

template <typename T>
struct Wrapper {
    // 只接受std::function
    static_assert(sizeof(T) < 0, "T must be a std::function");
};

所有能被sizeof計算的類型大小都不會比0小,所以這個斷言總會失敗,而且因為我們的斷言依賴模板參數,所以除非真的實例化這個模板,否則沒法判斷代碼是不是合法的,因此編譯期也不會觸發靜態斷言。

下麵是觸發斷言的結果:

g++ -std=c++20 -Wall -Wextra error.cpp

error.cpp: In instantiation of ‘struct Wrapper<int>’:
error.cpp:20:18:   required from here
error.cpp:6:29: 錯誤:static assertion failed: T must be a std::function
    6 |     static_assert(sizeof(T) < 0, "T must be a std::function");
      |                   ~~~~~~~~~~^~~
error.cpp:6:29: 附註:the comparison reduces to ‘(4 < 0)’
error.cpp: 在函數‘int main()’中:
error.cpp:20:18: 警告:unused variable ‘w’ [-Wunused-variable]
   20 |     Wrapper<int> w;
      |                  ^

現在靜態斷言可以如我們預期的那樣工作了。

總結

要想避免static_assert不按我們預期的情況來工作,需要遵守下麵的原則:

  • 儘量別用原始指針
  • 儘量少用引用,多使用值語義
  • 模板里要用到的東西儘量要和類型參數相關,尤其是靜態斷言
參考

https://stackoverflow.com/questions/5246049/c11-static-assert-and-template-instantiation

https://blog.knatten.org/2018/10/19/static_assert-in-templates/


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文章沒有任何宣傳產品等盈利性質,所有的陳述均為博主真實的日常使用。 關於電腦 現在很多學電腦的同學總是抱怨自己的電腦卡卡卡,打開一看360,金山等安全軟體在瘋狂打架,桌面各種文件堆得也是稀碎,再打開文件資源管理器一看,好家伙,C盤都紅了,各種文件堆在盤裡也是亂七八糟。清理垃圾本身沒多煩,21世紀 ...
  • 一、安裝VMware Workstation虛擬機 下載VMware Workstation 16 PRO虛擬機 https://www.vmware.com/cn/products/workstation-pro/workstation-pro-evaluation.html,下載後安裝即可,安裝 ...
  • 1.虛擬機下載 官網下載地址:https://www.kali.org/get-kali/#kali-virtual-machines 選擇VMware版本下載,並解壓 2.打開虛擬機 選擇打開虛擬機,瀏覽到剛纔壓縮包解壓路徑,選擇.vmx文件打開 開啟此虛擬機 用戶名密碼都是kali 3.設置ro ...
  • 世界上的開源許可證(Open Source License)大概有上百種,而我們常用的開源軟體協議大致有GPL、BSD、MIT、Mozilla、Apache和LGPL。 從下圖中可以看出幾種開源軟體協議的區別。 以下是上述協議的簡單介紹: GPL GNU是GNU General Public Lic ...
  • GreatSQL社區原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。 GreatSQL是MySQL的國產分支版本,使用上與MySQL一致。 作者: 如常 Debezium Incremental snapshotting Introduction CDC(Change-Data-Captur ...
  • 資料庫面試測試題(一) 簡述當前主流RDBMS軟體有哪些?開源且跨平臺的資料庫軟體有哪些? 參考答案 當前主流的資料庫伺服器軟體有: Oracle 、 DB2 、 SQL SERVER 、MySQL 等 ,其中只有MySQL是既開源又跨平臺的資料庫服務軟體。 簡述MySQL資料庫的服務進程名、預設端 ...
  • 1、文本類指令 {{}}、v-text 都是用於綁定節點的文本; 二者區別:{{}}這種綁定值的方式在頁面會出現“{{}}”一閃而過的效果 解決{{}}在頁面出現一閃而過的辦法: // css: [v-cloak] { display: 'none' }// html <h1 v-cloak>{{m ...
  • 5.10 介面開發-分片上傳 第2-1-2章 傳統方式安裝FastDFS-附FastDFS常用命令 第2-1-3章 docker-compose安裝FastDFS,實現文件存儲服務 第2-1-5章 docker安裝MinIO實現文件存儲服務-springboot整合minio-minio全網最全的資 ...
一周排行
    -Advertisement-
    Play Games
  • 1、預覽地址:http://139.155.137.144:9012 2、qq群:801913255 一、前言 隨著網路的發展,企業對於信息系統數據的保密工作愈發重視,不同身份、角色對於數據的訪問許可權都應該大相徑庭。 列如 1、不同登錄人員對一個數據列表的可見度是不一樣的,如數據列、數據行、數據按鈕 ...
  • 前言 上一篇文章寫瞭如何使用RabbitMQ做個簡單的發送郵件項目,然後評論也是比較多,也是準備去學習一下如何確保RabbitMQ的消息可靠性,但是由於時間原因,先來說說設計模式中的簡單工廠模式吧! 在瞭解簡單工廠模式之前,我們要知道C#是一款面向對象的高級程式語言。它有3大特性,封裝、繼承、多態。 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 介紹 Nodify是一個WPF基於節點的編輯器控制項,其中包含一系列節點、連接和連接器組件,旨在簡化構建基於節點的工具的過程 ...
  • 創建一個webapi項目做測試使用。 創建新控制器,搭建一個基礎框架,包括獲取當天日期、wiki的請求地址等 創建一個Http請求幫助類以及方法,用於獲取指定URL的信息 使用http請求訪問指定url,先運行一下,看看返回的內容。內容如圖右邊所示,實際上是一個Json數據。我們主要解析 大事記 部 ...
  • 最近在不少自媒體上看到有關.NET與C#的資訊與評價,感覺大家對.NET與C#還是不太瞭解,尤其是對2016年6月發佈的跨平臺.NET Core 1.0,更是知之甚少。在考慮一番之後,還是決定寫點東西總結一下,也回顧一下.NET的發展歷史。 首先,你沒看錯,.NET是跨平臺的,可以在Windows、 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 添加節點(nodes) 通過上一篇我們已經創建好了編輯器實例現在我們為編輯器添加一個節點 添加model和viewmode ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...
  • 類型檢查和轉換:當你需要檢查對象是否為特定類型,並且希望在同一時間內將其轉換為那個類型時,模式匹配提供了一種更簡潔的方式來完成這一任務,避免了使用傳統的as和is操作符後還需要進行額外的null檢查。 複雜條件邏輯:在處理複雜的條件邏輯時,特別是涉及到多個條件和類型的情況下,使用模式匹配可以使代碼更 ...
  • 在日常開發中,我們經常需要和文件打交道,特別是桌面開發,有時候就會需要載入大批量的文件,而且可能還會存在部分文件缺失的情況,那麼如何才能快速的判斷文件是否存在呢?如果處理不當的,且文件數量比較多的時候,可能會造成卡頓等情況,進而影響程式的使用體驗。今天就以一個簡單的小例子,簡述兩種不同的判斷文件是否... ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...