欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

記一次ADL導(dǎo)致的C++代碼編譯錯(cuò)誤的原因及解決方法

 更新時(shí)間:2025年07月07日 09:03:40   作者:apocelipes  
文章分析C++ ADL機(jī)制導(dǎo)致的print函數(shù)沖突,因<iostream>間接包含<print>頭文件,建議重命名或使用限定名稱解決,并強(qiáng)調(diào)IDE提示減少冗余頭文件以避免編譯問題,感興趣的朋友一起看看吧

這篇文章主要講講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è)計(jì)模式

    C++示例講解觀察者設(shè)計(jì)模式

    觀察者模式是極其重要的一個(gè)設(shè)計(jì)模式,也是我?guī)啄觊_發(fā)過程中使用最多的設(shè)計(jì)模式,本文首先概述觀察者模式的基本概念和Demo實(shí)現(xiàn),接著是觀察者模式在C++中的應(yīng)用,最后是對(duì)觀察者模式的應(yīng)用場(chǎng)景和優(yōu)缺點(diǎn)進(jìn)行總結(jié)
    2022-12-12
  • C語言實(shí)現(xiàn)統(tǒng)計(jì)100以內(nèi)所有素?cái)?shù)的個(gè)數(shù)

    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-11
  • C語言學(xué)生成績(jī)管理系統(tǒng)課程設(shè)計(jì)

    C語言學(xué)生成績(jī)管理系統(tǒng)課程設(shè)計(jì)

    這篇文章主要為大家詳細(xì)介紹了C語言學(xué)生成績(jī)管理系統(tǒng)課程設(shè)計(jì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-01-01
  • floyd算法實(shí)現(xiàn)思路及實(shí)例代碼

    floyd算法實(shí)現(xiàn)思路及實(shí)例代碼

    這篇文章主要介紹了floyd算法實(shí)現(xiàn)思路及實(shí)例代碼,有需要的朋友可以參考一下
    2014-01-01
  • C++實(shí)現(xiàn)list增刪查改模擬的示例代碼

    C++實(shí)現(xiàn)list增刪查改模擬的示例代碼

    本文主要介紹了C++實(shí)現(xiàn)list增刪查改模擬,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-12-12
  • 用C語言實(shí)現(xiàn)簡(jiǎn)單掃雷游戲

    用C語言實(shí)現(xiàn)簡(jiǎn)單掃雷游戲

    這篇文章主要為大家詳細(xì)介紹了用C語言實(shí)現(xiàn)簡(jiǎn)單掃雷游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-07-07
  • C語言實(shí)現(xiàn)密碼強(qiáng)度檢測(cè)

    C語言實(shí)現(xiàn)密碼強(qiáng)度檢測(cè)

    這篇文章主要為大家詳細(xì)介紹了C語言實(shí)現(xiàn)密碼強(qiáng)度檢測(cè),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2020-03-03
  • C++的多態(tài)和虛函數(shù)你真的了解嗎

    C++的多態(tài)和虛函數(shù)你真的了解嗎

    這篇文章主要為大家詳細(xì)介紹了C++的多態(tài)和虛函數(shù),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助
    2022-02-02
  • C語言全方位講解指針的使用

    C語言全方位講解指針的使用

    指針是C語言中一個(gè)非常重要的概念,也是C語言的特色之一。使用指針可以對(duì)復(fù)雜數(shù)據(jù)進(jìn)行處理,能對(duì)計(jì)算機(jī)的內(nèi)存分配進(jìn)行控制,在函數(shù)調(diào)用中使用指針還可以返回多個(gè)值
    2022-04-04
  • C++中String增刪查改模擬實(shí)現(xiàn)方法舉例

    C++中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

最新評(píng)論