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 de?nition 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/
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/536858.html
標籤:其他
