亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

記一次ADL導致的C++代碼編譯錯誤的原因及解決方法

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

這篇文章主要講講c++的ADL,順便說說為什么很多c++的IDE都會讓你盡量不要include用不上的頭文件。

和其他c++文章一樣,這篇也會有基礎回顧環(huán)節(jié),所以不用擔心看不懂,但讀者最好還是得有c++的基礎知識并且對c++11之后的內容有所了解。

好了,下面我們進入正題吧。

偶遇報錯

最近工作收尾有了不少空閑時間,于是準備試試手頭環(huán)境的編譯器對新標準的支持,以便選擇合適的時機給自己的幾個項目做個升級。

雖然有現(xiàn)成的工具的網(wǎng)站可以查詢編譯器對新標準的支持情況,但這些網(wǎng)站給的信息還是不夠詳細,有時候得寫些例子手動編譯做測試。我是個懶人,所以我不愿意花時間自己寫,而AI又對新標準理解的不夠透徹,可能是語料太少的緣故,總是寫出點離譜的東西。無奈之下我只能去網(wǎng)上找現(xiàn)成的吃了,cppreference是個不錯的選擇,用的人很多而且比較權威,更棒的是對于新特性它一般都給出了示例代碼,這正中我的下懷。

于是我搬了這樣一段代碼進行測試,預想中要么編譯成功要么新特性不支持導致編譯失敗:

#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);
}

很簡單的代碼,測試一下c++23的ranges::views::zip,如果要報錯那么多半也是和這個zip有關。

然而事實出人意料:

$ 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ù)報錯了,和zip完全不相關,難道說cppreference上例子會有這么明顯的錯誤?但檢查了一下print也只用到了早就支持的c++20的語法并不存在錯誤,而且換成gcc和Linux上的clang18之后都能正常編譯。

這還只是第一個點異常,仔細閱讀報錯信息就會發(fā)現(xiàn)第二點了:我們沒有導入c++23的新標準庫<print>,為什么我們自定義的print會和std::print沖突呢?

看到這里是不是已經(jīng)按耐不住自己想轉投Rust的心了?不過別急,盡管報錯很離奇但原因沒那么復雜,聽我慢慢解釋。

基礎回顧

基礎回顧是c++博客少不了的環(huán)節(jié),因為語法太多太瑣碎,不回顧下容易看不懂后續(xù)的內容。

限定和非限定名稱

第一個要回顧的是限定名稱非限定名稱這兩個概念。國內有時候也會把非限定名稱叫做無限定名稱,我覺得后者更符合中文的語用習慣,不過我這兒一直非限定非限定的習慣了所以就不改了。

如果要照著標準規(guī)范念經(jīng),那可有得念了,所以我會有通俗易懂的方式解釋,這樣多少會和真正的標準有那么點出入,還請語言律師們海涵。

簡單的說,c++里如果一個標識符光禿禿的,比如print,那么它是非限定名稱;而如果一個名字前面包含命名空間限定符,比如::print, std::print, classA::print,那么它是限定名稱。

他倆有啥區(qū)別呢?限定名稱的限定指的是指定了這標識符出現(xiàn)在那個命名空間/類里,編譯器只能去限定的地方查找,沒找到就是編譯錯誤。而非限定名稱,因為沒限制編譯器去哪找這個標識符,所以編譯器會從當前作用域開始,一路往上走查找每個父作用域/類以找到這個標識符,注意同級的命名空間/類不會進行搜索。

舉個例子:

#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
            // 下面這行會報錯,因為使用了限定名稱,只允許編譯器搜索B,B中沒有a
            // std::cout << B::a << '\n';
        }
    }
}
int main()
{
    A::B::print(); // 這也是限定名稱
    // 輸出 3 1 2
}

順帶一提每個編譯單元都有一個默認存在的匿名的命名空間,所有沒有明確定義在其他命名空間中的標識符都會被歸入這個匿名的命名空間。舉個例子,前文里我們定義的print函數(shù)就是在這個匿名的命名空間中,這個空間和std是平級關系。

非限定名稱可以讓程序員以自然的方式引入外層作用域的名字,而限定名稱則提供了一個防止名稱沖突的機制。

ADL

理解了限定和非限定名稱,下面我們再看看這行代碼:

std::cout << A::b << '\n';

注意那個<<,c++允許進行運算符重載,所以它的真身其實是std::ostream& operator<<(...),并且這個運算符是定義在std這個命名空間中的。

因為我們沒有限定運算符的命名空間(按照運算符當前的調用方式我們也沒法進行限定),所以編譯器會從當前作用域開始逐層往上查找。但我們的代碼中沒有定義過這個運算符,std則不在非限定名稱的搜索范圍內,理論上編譯器不應該報錯說找不到operator<<嗎?

事實上程序可以正常編譯,因為c++還有另外一套名稱查找策略,叫ADL——Argument Dependent Lookup。

簡單的說,如果一個函數(shù)/運算符是非限定名稱,而它的實際參數(shù)的類型所在的命名空間里定義有同名的函數(shù),那么編譯器就會把這個和實參類型在同一空間的函數(shù)當成這個非限定名稱指代的函數(shù)/運算符。當然真實環(huán)境下編譯器還得考慮可見性和函數(shù)重載決議,這里我們不細究了。

還是以上面那行代碼為例,雖然我們沒有重載<<,但<iostream>里有在std里重載,而我們的實際參數(shù)是std::cout,類型是std::ostream&,所以ADL會去命名空間std中查找是否有符合調用形式的operator<<,編譯器會發(fā)現(xiàn)正好有完全合適的運算符存在,所以編譯成功不會報錯。

另外ADL只適用于函數(shù)和運算符(也算一種特殊的函數(shù)),lambda、functor等東西觸發(fā)不了ADL。

ADL最大的用處是方便了運算符重載的使用。否則,我們不得不寫很多std::operator<<(a, b)這樣的代碼,這既繁瑣又不符合自然習慣。此外c++還有一些基于ADL的慣用法,例如我之前介紹過的copy-and-swap慣用法。

不過除了少數(shù)正面作用,ADL更多的時候是個trouble maker,本文開頭那個報錯就是活生生的例子。

報錯原因

復習完基礎我們再看報錯信息:

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里的容器類的實例,print是非限定名稱,于是非限定名稱的查找觸發(fā),找到了我們定義的print,ADL也被觸發(fā),因為編譯器要找出所有可行的函數(shù)或者函數(shù)模板然后用重載決議確定調用哪一個,于是c++23的新函數(shù)std::print被找到。

不巧的是兩個函數(shù)雖然參數(shù)形式不太一樣,但誰也不比誰更特殊化,導致出現(xiàn)調用的二義性,編譯器不知道該用我們的模板函數(shù)還是標準庫的,報錯了。

正是ADL把我們不需要的函數(shù)加入了重載決議過程,cppreference上那段代碼才會報錯。

排查和處理

首先要排查問題是誰引起的。

看起來鍋全是ADL的,但引入了<print>的家伙其實要分一半的鍋,因為不引入這東西我們的代碼里是沒有std::print的,編譯器就算用了ADL也不會看到這個干擾項。

那么多頭文件,一個個看是看不完根本看不完。不過我們能縮小范圍。

std::print是輸出相關的,標準庫實際上有一定要求不能隨便亂include文件,所以我們可以先鎖定<iostream>;其次標準庫的容器有時候會對一些模板做特殊化,這些特殊化的模板當然也能被ADL找出來,所以容器的頭文件也需要檢查,萬一他們特殊處理了std::print也說不定,不過鑒于vector,array,list都報錯了,那說明我們只需要看其中一個就行,我選擇<array>,因為比起另外兩個std::array的結構更簡單功能相對也少一些,所以代碼也相對更少更方便檢查。

我先檢查了<array>和它include的所有文件,并未發(fā)現(xiàn)<print>。

所以我又檢查了<iostream>,bingo,罪魁禍首是它include的<ostream>

#if _LIBCPP_STD_VER >= 23
#  include <__ostream/print.h>
#endif

檢測到在用c++23就導入<__ostream/print.h>,而這個頭文件里直接#include <print>了。

原因找到,現(xiàn)在該想想如何修復了。

修起來也簡單,要么讓我們自定義的print更加特殊使其在重載決議中勝出,要么使用限定名稱直接屏蔽掉std,或者干脆給函數(shù)改個名字。

我只是想試試編譯器支不支持新的ranges函數(shù),懶勁發(fā)作不想動腦子,所以選了第二種,畢竟加個::就完事了:

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正常編譯,不再會報錯。

為什么不能亂include

現(xiàn)代C++ IDE一般都會在你include沒用的頭文件時給出提示或警告,這不僅僅是因為會拖累編譯速度。

上面的例子告訴你了:include了沒用的東西有時候會影響c++的名稱查找導致莫名其妙的錯誤。

但話說回來,同樣的代碼g++并未報錯,為啥呢,因為g++用的libstdc++直接實現(xiàn)了std::printstd::ostream的重載,而沒#include <print>,事實上從libstdc++的代碼來看這個include也沒有必要。Linux上的clang除非特殊指定否則和g++用的同一套標準庫代碼,所以沒有報錯。macOS上的clang用的是libcxx,就遇上問題了。

當然我沒看libcxx的代碼不好說它這個include是對是錯,也許它的代碼里不得不這樣做也未可知。

總結

c++就像古神,要不是我正好熟悉這塊的語言規(guī)則好奇心也比較重,這個詭異的報錯就要讓我陷入瘋狂了。

cppreference上的例子如果有人有興趣可以嘗試下修改,推薦選擇給print函數(shù)重命名這個方案,這也是為社區(qū)做貢獻的一次好機會。鏈接在這里:link

當然我懶抽筋了,這個機會就讓給有緣人嘍。

到此這篇關于記一次ADL導致的C++代碼編譯錯誤的原因及解決方法的文章就介紹到這了,更多相關C++代碼編譯錯誤內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • C++示例講解觀察者設計模式

    C++示例講解觀察者設計模式

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

    C語言實現(xiàn)統(tǒng)計100以內所有素數(shù)的個數(shù)

    本文詳細講解了C語言實現(xiàn)統(tǒng)計100以內所有素數(shù)個數(shù)的方法,文中通過示例代碼介紹的非常詳細。需要的朋友可以收藏下,方便下次瀏覽觀看
    2021-11-11
  • C語言學生成績管理系統(tǒng)課程設計

    C語言學生成績管理系統(tǒng)課程設計

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

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

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

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

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

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

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

    C語言實現(xiàn)密碼強度檢測

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

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

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

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

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

    C++中String增刪查改模擬實現(xiàn)方法舉例

    這篇文章主要給大家介紹了關于C++中String增刪查改模擬實現(xiàn)方法的相關資料,String是C++中的重要類型,程序員在C++面試中經(jīng)常會遇到關于String的細節(jié)問題,甚至要求當場實現(xiàn)這個類,需要的朋友可以參考下
    2023-11-11

最新評論