LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

記一次ADL導致的C++代碼編譯錯誤

freeflydom
2025年7月12日 17:14 本文熱度 375

省流:用不到的不要include進來。

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

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

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

偶遇報錯

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

雖然有現成的工具的網站可以查詢編譯器對新標準的支持情況,但這些網站給的信息還是不夠詳細,有時候得寫些例子手動編譯做測試。我是個懶人,所以我不愿意花時間自己寫,而AI又對新標準理解的不夠透徹,可能是語料太少的緣故,總是寫出點離譜的東西。無奈之下我只能去網上找現成的吃了,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函數報錯了,和zip完全不相關,難道說cppreference上例子會有這么明顯的錯誤?但檢查了一下print也只用到了早就支持的c++20的語法并不存在錯誤,而且換成gcc和Linux上的clang18之后都能正常編譯。

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

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

基礎回顧

基礎回顧是c++博客少不了的環節,因為語法太多太瑣碎,不回顧下容易看不懂后續的內容。

限定和非限定名稱

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

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

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

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

舉個例子:

#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函數就是在這個匿名的命名空間中,這個空間和std是平級關系。

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

ADL

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

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

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

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

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

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

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

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

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

不過除了少數正面作用,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是非限定名稱,于是非限定名稱的查找觸發,找到了我們定義的print,ADL也被觸發,因為編譯器要找出所有可行的函數或者函數模板然后用重載決議確定調用哪一個,于是c++23的新函數std::print被找到。

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

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

排查和處理

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

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

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

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

我先檢查了<array>和它include的所有文件,并未發現<print>

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

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

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

原因找到,現在該想想如何修復了。

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

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

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

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

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

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

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

總結

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

轉自https://www.cnblogs.com/apocelipes/p/18968246


該文章在 2025/7/12 17:14:59 編輯過
關鍵字查詢
相關文章
正在查詢...
點晴ERP是一款針對中小制造業的專業生產管理軟件系統,系統成熟度和易用性得到了國內大量中小企業的青睞。
點晴PMS碼頭管理系統主要針對港口碼頭集裝箱與散貨日常運作、調度、堆場、車隊、財務費用、相關報表等業務管理,結合碼頭的業務特點,圍繞調度、堆場作業而開發的。集技術的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業的高效ERP管理信息系統。
點晴WMS倉儲管理系統提供了貨物產品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質期管理,貨位管理,庫位管理,生產管理,WMS管理系統,標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務都免費,不限功能、不限時間、不限用戶的免費OA協同辦公管理系統。
Copyright 2010-2025 ClickSun All Rights Reserved

黄频国产免费高清视频,久久不卡精品中文字幕一区,激情五月天AV电影在线观看,欧美国产韩国日本一区二区
伊人久久大香线蕉精品 | 日本欧美高清乱码一区二区 | 午夜在线观看的那种网站 | 亚洲伊人久久综合精品 | 亚洲人午夜网站在线播放 | 中国三级久久精品三级 |