記一次ADL導(dǎo)致的C++代碼編譯錯(cuò)誤的原因及解決方法
這篇文章主要講講c++的ADL,順便說說為什么很多c++的IDE都會(huì)讓你盡量不要include用不上的頭文件。
和其他c++文章一樣,這篇也會(huì)有基礎(chǔ)回顧環(huán)節(jié),所以不用擔(dān)心看不懂,但讀者最好還是得有c++的基礎(chǔ)知識(shí)并且對(duì)c++11之后的內(nèi)容有所了解。
好了,下面我們進(jìn)入正題吧。
偶遇報(bào)錯(cuò)
最近工作收尾有了不少空閑時(shí)間,于是準(zhǔn)備試試手頭環(huán)境的編譯器對(duì)新標(biāo)準(zhǔn)的支持,以便選擇合適的時(shí)機(jī)給自己的幾個(gè)項(xiàng)目做個(gè)升級(jí)。
雖然有現(xiàn)成的工具的網(wǎng)站可以查詢編譯器對(duì)新標(biāo)準(zhǔn)的支持情況,但這些網(wǎng)站給的信息還是不夠詳細(xì),有時(shí)候得寫些例子手動(dòng)編譯做測(cè)試。我是個(gè)懶人,所以我不愿意花時(shí)間自己寫,而AI又對(duì)新標(biāo)準(zhǔn)理解的不夠透徹,可能是語料太少的緣故,總是寫出點(diǎn)離譜的東西。無奈之下我只能去網(wǎng)上找現(xiàn)成的吃了,cppreference是個(gè)不錯(cuò)的選擇,用的人很多而且比較權(quán)威,更棒的是對(duì)于新特性它一般都給出了示例代碼,這正中我的下懷。
于是我搬了這樣一段代碼進(jìn)行測(cè)試,預(yù)想中要么編譯成功要么新特性不支持導(dǎo)致編譯失?。?/p>
#include <array> #include <iostream> #include <list> #include <ranges> #include <string> #include <tuple> #include <vector> void print(auto const rem, auto const& range) { for (std::cout << rem; auto const& elem : range) std::cout << elem << ' '; std::cout << '\n'; } int main() { auto x = std::vector{1, 2, 3, 4}; auto y = std::list<std::string>{"α", "β", "γ", "δ", "ε"}; auto z = std::array{'A', 'B', 'C', 'D', 'E', 'F'}; print("Source views:", ""); print("x: ", x); print("y: ", y); print("z: ", z); print("\nzip(x,y,z):", ""); for (std::tuple<int&, std::string&, char&> elem : std::views::zip(x, y, z)) { std::cout << std::get<0>(elem) << ' ' << std::get<1>(elem) << ' ' << std::get<2>(elem) << '\n'; std::get<char&>(elem) += ('a' - 'A'); // modifies the element of z } print("\nAfter modification, z: ", z); }
很簡(jiǎn)單的代碼,測(cè)試一下c++23的ranges::views::zip
,如果要報(bào)錯(cuò)那么多半也是和這個(gè)zip有關(guān)。
然而事實(shí)出人意料:
$ clang++ -std=c++23 -Wall test.cpp test.cpp:23:5: error: call to 'print' is ambiguous 23 | print("x: ", x); | ^~~~~ test.cpp:9:6: note: candidate function [with rem:auto = const char *, range:auto = std::vector<int>] 9 | void print(auto const rem, auto const& range) | ^ /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/print:343:28: note: candidate function [with _Args = <std::vector<int> &>] 343 | _LIBCPP_HIDE_FROM_ABI void print(format_string<_Args...> __fmt, _Args&&... __args) { | ^ test.cpp:24:5: error: call to 'print' is ambiguous 24 | print("y: ", y); | ^~~~~ test.cpp:9:6: note: candidate function [with rem:auto = const char *, range:auto = std::list<std::string>] 9 | void print(auto const rem, auto const& range) | ^ /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/print:343:28: note: candidate function [with _Args = <std::list<std::string> &>] 343 | _LIBCPP_HIDE_FROM_ABI void print(format_string<_Args...> __fmt, _Args&&... __args) { | ^ test.cpp:25:5: error: call to 'print' is ambiguous 25 | print("z: ", z); | ^~~~~ test.cpp:9:6: note: candidate function [with rem:auto = const char *, range:auto = std::array<char, 6>] 9 | void print(auto const rem, auto const& range) | ^ /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/print:343:28: note: candidate function [with _Args = <std::array<char, 6> &>] 343 | _LIBCPP_HIDE_FROM_ABI void print(format_string<_Args...> __fmt, _Args&&... __args) { | ^ test.cpp:38:5: error: call to 'print' is ambiguous 38 | print("\nAfter modification, z: ", z); | ^~~~~ test.cpp:9:6: note: candidate function [with rem:auto = const char *, range:auto = std::array<char, 6>] 9 | void print(auto const rem, auto const& range) | ^ /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/print:343:28: note: candidate function [with _Args = <std::array<char, 6> &>] 343 | _LIBCPP_HIDE_FROM_ABI void print(format_string<_Args...> __fmt, _Args&&... __args) { | ^ 4 errors generated.
print函數(shù)報(bào)錯(cuò)了,和zip完全不相關(guān),難道說cppreference上例子會(huì)有這么明顯的錯(cuò)誤?但檢查了一下print
也只用到了早就支持的c++20的語法并不存在錯(cuò)誤,而且換成gcc和Linux上的clang18之后都能正常編譯。
這還只是第一個(gè)點(diǎn)異常,仔細(xì)閱讀報(bào)錯(cuò)信息就會(huì)發(fā)現(xiàn)第二點(diǎn)了:我們沒有導(dǎo)入c++23的新標(biāo)準(zhǔn)庫<print>
,為什么我們自定義的print
會(huì)和std::print
沖突呢?
看到這里是不是已經(jīng)按耐不住自己想轉(zhuǎn)投Rust的心了?不過別急,盡管報(bào)錯(cuò)很離奇但原因沒那么復(fù)雜,聽我慢慢解釋。
基礎(chǔ)回顧
基礎(chǔ)回顧是c++博客少不了的環(huán)節(jié),因?yàn)檎Z法太多太瑣碎,不回顧下容易看不懂后續(xù)的內(nèi)容。
限定和非限定名稱
第一個(gè)要回顧的是限定名稱
和非限定名稱
這兩個(gè)概念。國(guó)內(nèi)有時(shí)候也會(huì)把非限定名稱叫做無限定名稱,我覺得后者更符合中文的語用習(xí)慣,不過我這兒一直非限定非限定的習(xí)慣了所以就不改了。
如果要照著標(biāo)準(zhǔn)規(guī)范念經(jīng),那可有得念了,所以我會(huì)有通俗易懂的方式解釋,這樣多少會(huì)和真正的標(biāo)準(zhǔn)有那么點(diǎn)出入,還請(qǐng)語言律師們海涵。
簡(jiǎn)單的說,c++里如果一個(gè)標(biāo)識(shí)符光禿禿的,比如print
,那么它是非限定名稱;而如果一個(gè)名字前面包含命名空間限定符,比如::print, std::print, classA::print
,那么它是限定名稱。
他倆有啥區(qū)別呢?限定名稱的限定指的是指定了這標(biāo)識(shí)符出現(xiàn)在那個(gè)命名空間/類里,編譯器只能去限定的地方查找,沒找到就是編譯錯(cuò)誤。而非限定名稱,因?yàn)闆]限制編譯器去哪找這個(gè)標(biāo)識(shí)符,所以編譯器會(huì)從當(dāng)前作用域開始,一路往上走查找每個(gè)父作用域/類以找到這個(gè)標(biāo)識(shí)符,注意同級(jí)的命名空間/類不會(huì)進(jìn)行搜索。
舉個(gè)例子:
#include <iostream> namespace A { int a = 1; int b = 2; namespace B { int b = 3; void print() { std::cout << b << '\n'; // 非限定名稱,就近找到A::B::b std::cout << a << '\n'; // 非限定名稱,找到父命名空間的A::a std::cout << A::b << '\n'; // 限定名稱,直接找到A::b // 下面這行會(huì)報(bào)錯(cuò),因?yàn)槭褂昧讼薅Q,只允許編譯器搜索B,B中沒有a // std::cout << B::a << '\n'; } } } int main() { A::B::print(); // 這也是限定名稱 // 輸出 3 1 2 }
順帶一提每個(gè)編譯單元都有一個(gè)默認(rèn)存在的匿名的命名空間,所有沒有明確定義在其他命名空間中的標(biāo)識(shí)符都會(huì)被歸入這個(gè)匿名的命名空間。舉個(gè)例子,前文里我們定義的print
函數(shù)就是在這個(gè)匿名的命名空間中,這個(gè)空間和std
是平級(jí)關(guān)系。
非限定名稱可以讓程序員以自然的方式引入外層作用域的名字,而限定名稱則提供了一個(gè)防止名稱沖突的機(jī)制。
ADL
理解了限定和非限定名稱,下面我們?cè)倏纯催@行代碼:
std::cout << A::b << '\n';
注意那個(gè)<<
,c++允許進(jìn)行運(yùn)算符重載,所以它的真身其實(shí)是std::ostream& operator<<(...)
,并且這個(gè)運(yùn)算符是定義在std
這個(gè)命名空間中的。
因?yàn)槲覀儧]有限定運(yùn)算符的命名空間(按照運(yùn)算符當(dāng)前的調(diào)用方式我們也沒法進(jìn)行限定),所以編譯器會(huì)從當(dāng)前作用域開始逐層往上查找。但我們的代碼中沒有定義過這個(gè)運(yùn)算符,std則不在非限定名稱的搜索范圍內(nèi),理論上編譯器不應(yīng)該報(bào)錯(cuò)說找不到operator<<
嗎?
事實(shí)上程序可以正常編譯,因?yàn)閏++還有另外一套名稱查找策略,叫ADL——Argument Dependent Lookup。
簡(jiǎn)單的說,如果一個(gè)函數(shù)/運(yùn)算符是非限定名稱,而它的實(shí)際參數(shù)的類型所在的命名空間里定義有同名的函數(shù),那么編譯器就會(huì)把這個(gè)和實(shí)參類型在同一空間的函數(shù)當(dāng)成這個(gè)非限定名稱指代的函數(shù)/運(yùn)算符。當(dāng)然真實(shí)環(huán)境下編譯器還得考慮可見性和函數(shù)重載決議,這里我們不細(xì)究了。
還是以上面那行代碼為例,雖然我們沒有重載<<
,但<iostream>
里有在std里重載,而我們的實(shí)際參數(shù)是std::cout
,類型是std::ostream&
,所以ADL會(huì)去命名空間std中查找是否有符合調(diào)用形式的operator<<
,編譯器會(huì)發(fā)現(xiàn)正好有完全合適的運(yùn)算符存在,所以編譯成功不會(huì)報(bào)錯(cuò)。
另外ADL只適用于函數(shù)和運(yùn)算符(也算一種特殊的函數(shù)),lambda、functor等東西觸發(fā)不了ADL。
ADL最大的用處是方便了運(yùn)算符重載的使用。否則,我們不得不寫很多std::operator<<(a, b)
這樣的代碼,這既繁瑣又不符合自然習(xí)慣。此外c++還有一些基于ADL的慣用法,例如我之前介紹過的copy-and-swap慣用法。
不過除了少數(shù)正面作用,ADL更多的時(shí)候是個(gè)trouble maker,本文開頭那個(gè)報(bào)錯(cuò)就是活生生的例子。
報(bào)錯(cuò)原因
復(fù)習(xí)完基礎(chǔ)我們?cè)倏磮?bào)錯(cuò)信息:
test.cpp:23:5: error: call to 'print' is ambiguous 23 | print("x: ", x); | ^~~~~ test.cpp:9:6: note: candidate function [with rem:auto = const char *, range:auto = std::vector<int>] 9 | void print(auto const rem, auto const& range) | ^ /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/print:343:28: note: candidate function [with _Args = <std::vector<int> &>] 343 | _LIBCPP_HIDE_FROM_ABI void print(format_string<_Args...> __fmt, _Args&&... __args) { | ^
我們的x,y,z
都是std里的容器類的實(shí)例,print
是非限定名稱,于是非限定名稱的查找觸發(fā),找到了我們定義的print,ADL也被觸發(fā),因?yàn)榫幾g器要找出所有可行的函數(shù)或者函數(shù)模板然后用重載決議確定調(diào)用哪一個(gè),于是c++23的新函數(shù)std::print
被找到。
不巧的是兩個(gè)函數(shù)雖然參數(shù)形式不太一樣,但誰也不比誰更特殊化,導(dǎo)致出現(xiàn)調(diào)用的二義性,編譯器不知道該用我們的模板函數(shù)還是標(biāo)準(zhǔn)庫的,報(bào)錯(cuò)了。
正是ADL把我們不需要的函數(shù)加入了重載決議過程,cppreference上那段代碼才會(huì)報(bào)錯(cuò)。
排查和處理
首先要排查問題是誰引起的。
看起來鍋全是ADL的,但引入了<print>
的家伙其實(shí)要分一半的鍋,因?yàn)椴灰脒@東西我們的代碼里是沒有std::print
的,編譯器就算用了ADL也不會(huì)看到這個(gè)干擾項(xiàng)。
那么多頭文件,一個(gè)個(gè)看是看不完根本看不完。不過我們能縮小范圍。
std::print
是輸出相關(guān)的,標(biāo)準(zhǔn)庫實(shí)際上有一定要求不能隨便亂include文件,所以我們可以先鎖定<iostream>
;其次標(biāo)準(zhǔn)庫的容器有時(shí)候會(huì)對(duì)一些模板做特殊化,這些特殊化的模板當(dāng)然也能被ADL找出來,所以容器的頭文件也需要檢查,萬一他們特殊處理了std::print
也說不定,不過鑒于vector,array,list都報(bào)錯(cuò)了,那說明我們只需要看其中一個(gè)就行,我選擇<array>
,因?yàn)楸绕鹆硗鈨蓚€(gè)std::array
的結(jié)構(gòu)更簡(jiǎn)單功能相對(duì)也少一些,所以代碼也相對(duì)更少更方便檢查。
我先檢查了<array>
和它include的所有文件,并未發(fā)現(xiàn)<print>
。
所以我又檢查了<iostream>
,bingo,罪魁禍?zhǔn)资撬黫nclude的<ostream>
:
#if _LIBCPP_STD_VER >= 23 # include <__ostream/print.h> #endif
檢測(cè)到在用c++23就導(dǎo)入<__ostream/print.h>
,而這個(gè)頭文件里直接#include <print>
了。
原因找到,現(xiàn)在該想想如何修復(fù)了。
修起來也簡(jiǎn)單,要么讓我們自定義的print更加特殊使其在重載決議中勝出,要么使用限定名稱直接屏蔽掉std,或者干脆給函數(shù)改個(gè)名字。
我只是想試試編譯器支不支持新的ranges
函數(shù),懶勁發(fā)作不想動(dòng)腦子,所以選了第二種,畢竟加個(gè)::
就完事了:
int main() { auto x = std::vector{1, 2, 3, 4}; auto y = std::list<std::string>{"α", "β", "γ", "δ", "ε"}; auto z = std::array{'A', 'B', 'C', 'D', 'E', 'F'}; - print("Source views:", ""); - print("x: ", x); - print("y: ", y); - print("z: ", z); + ::print("Source views:", ""); + ::print("x: ", x); + ::print("y: ", y); + ::print("z: ", z); - print("\nzip(x,y,z):", ""); + ::print("\nzip(x,y,z):", ""); for (std::tuple<int&, std::string&, char&> elem : std::views::zip(x, y, z)) { std::cout << std::get<0>(elem) << ' ' << std::get<1>(elem) << ' ' << std::get<2>(elem) << '\n'; std::get<char&>(elem) += ('a' - 'A'); // modifies the element of z } - print("\nAfter modification, z: ", z); + ::print("\nAfter modification, z: ", z); }
修改后的代碼可以用g++和clang正常編譯,不再會(huì)報(bào)錯(cuò)。
為什么不能亂include
現(xiàn)代C++ IDE一般都會(huì)在你include沒用的頭文件時(shí)給出提示或警告,這不僅僅是因?yàn)闀?huì)拖累編譯速度。
上面的例子告訴你了:include了沒用的東西有時(shí)候會(huì)影響c++的名稱查找導(dǎo)致莫名其妙的錯(cuò)誤。
但話說回來,同樣的代碼g++并未報(bào)錯(cuò),為啥呢,因?yàn)間++用的libstdc++直接實(shí)現(xiàn)了std::print
對(duì)std::ostream
的重載,而沒#include <print>
,事實(shí)上從libstdc++的代碼來看這個(gè)include也沒有必要。Linux上的clang除非特殊指定否則和g++用的同一套標(biāo)準(zhǔn)庫代碼,所以沒有報(bào)錯(cuò)。macOS上的clang用的是libcxx,就遇上問題了。
當(dāng)然我沒看libcxx的代碼不好說它這個(gè)include是對(duì)是錯(cuò),也許它的代碼里不得不這樣做也未可知。
總結(jié)
c++就像古神,要不是我正好熟悉這塊的語言規(guī)則好奇心也比較重,這個(gè)詭異的報(bào)錯(cuò)就要讓我陷入瘋狂了。
cppreference上的例子如果有人有興趣可以嘗試下修改,推薦選擇給print
函數(shù)重命名這個(gè)方案,這也是為社區(qū)做貢獻(xiàn)的一次好機(jī)會(huì)。鏈接在這里:link
當(dāng)然我懶抽筋了,這個(gè)機(jī)會(huì)就讓給有緣人嘍。
到此這篇關(guān)于記一次ADL導(dǎo)致的C++代碼編譯錯(cuò)誤的原因及解決方法的文章就介紹到這了,更多相關(guān)C++代碼編譯錯(cuò)誤內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語言實(shí)現(xiàn)統(tǒng)計(jì)100以內(nèi)所有素?cái)?shù)的個(gè)數(shù)
本文詳細(xì)講解了C語言實(shí)現(xiàn)統(tǒng)計(jì)100以內(nèi)所有素?cái)?shù)個(gè)數(shù)的方法,文中通過示例代碼介紹的非常詳細(xì)。需要的朋友可以收藏下,方便下次瀏覽觀看2021-11-11C語言學(xué)生成績(jī)管理系統(tǒng)課程設(shè)計(jì)
這篇文章主要為大家詳細(xì)介紹了C語言學(xué)生成績(jī)管理系統(tǒng)課程設(shè)計(jì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01floyd算法實(shí)現(xiàn)思路及實(shí)例代碼
這篇文章主要介紹了floyd算法實(shí)現(xiàn)思路及實(shí)例代碼,有需要的朋友可以參考一下2014-01-01C++實(shí)現(xiàn)list增刪查改模擬的示例代碼
本文主要介紹了C++實(shí)現(xiàn)list增刪查改模擬,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-12-12C語言實(shí)現(xiàn)密碼強(qiáng)度檢測(cè)
這篇文章主要為大家詳細(xì)介紹了C語言實(shí)現(xiàn)密碼強(qiáng)度檢測(cè),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-03-03C++中String增刪查改模擬實(shí)現(xiàn)方法舉例
這篇文章主要給大家介紹了關(guān)于C++中String增刪查改模擬實(shí)現(xiàn)方法的相關(guān)資料,String是C++中的重要類型,程序員在C++面試中經(jīng)常會(huì)遇到關(guān)于String的細(xì)節(jié)問題,甚至要求當(dāng)場(chǎng)實(shí)現(xiàn)這個(gè)類,需要的朋友可以參考下2023-11-11