我有一份未經我證實但來自可靠來源的報告,該代碼
qsort(a, n, sizeof *a, cmpfunc);
由現代版本的 gcc 編譯,就好像它已經撰寫過一樣
if(n == 0)
__builtin_trap();
qsort(a, n, sizeof *a, cmpfunc);
顯然,人們認為呼叫qsortwithn == 0是未定義的行為。
[編輯:這里的整個前提被發現是錯誤的;請參閱下面的“更新 2”。]
有人指出,Posix 明確支持這種n == 0情況,但顯然沒有 C 標準的現存版本這樣做。
所以顯而易見的問題是:
qsort是否在 C 中以n = 0實際未定義的行為呼叫?- 每個曾經
qsort任意呼叫的程式是否n真的有義務檢查n == 0而不是qsort在這種情況下呼叫? - 為什么 gcc 會執行這種“優化”?即使您認為呼叫
qsortwithn == 0是未定義的,這似乎也會稍微減慢每個未定義的程式。
快速排序的教科書實作(我知道,qsort這不是必需的)幾乎無法n = 0正確處理。我想知道 gcc 在這里的行為是否試圖防止以qsort某種方式比__builtin_trap初始呼叫更糟糕的實作n == 0?
更新:感謝到目前為止的回復。聽起來 gcc 在這里是錯誤的。正如我所說,我自己還沒有確認這個結果,但我試圖找出哪個版本的 gcc 以及觀察到問題的優化標志。
更新 2:我提到的原始報告有誤。兩個關鍵說明:
- gcc 實際上是在檢查
a == 0,而不是n == 0。這顯然是完全不同的魚:正如這個執行緒(和其他執行緒)所證實的那樣,呼叫qsort空指標的問題要大得多,而且幾乎可以肯定在形式上是未定義的。 - 有問題的編譯包括
-fsanitize=undefinedand-fsanitize-undefined-trap-on-error標志,因此gcc 對檢查無意的空指標當然是嚴格的(甚至以效率為代價)。
對不起,錯誤資訊和跑路。恐怕這個問題現在屬于“不可重現或由拼寫錯誤引起”的領域,我在此基礎上對漏斗投了票。
值得一提的是,gcc 版本是 12.2.1。
uj5u.com熱心網友回復:
- 在 C 中呼叫 n = 0 的 qsort 實際上是未定義的行為嗎?
在每個版本的語言中,它都是明確定義的行為。
- 每個使用任意 n 呼叫 qsort 的程式是否真的有義務檢查 n == 0 并且在這種情況下不呼叫 qsort ?
應用程式員的源代碼不需要執行任何此類檢查。至于生成程式的行為,qsort庫函式內部不應該呼叫比較函式,所以本質上和根本不呼叫是一樣的qsort,相當于無操作。
為什么 gcc 會執行這種“優化”?即使您認為使用 n == 0 呼叫 qsort 是未定義的,這似乎也會稍微減慢每個未定義的程式。
因為 n == 0 是一種特殊的、定義明確的情況,它允許編譯器優化(而不是呼叫函式)。當然,額外的分支不一定是優化。
資料來源:
C17 7.22.5.2
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
C17 7.22.5 強調地雷:
這些實用程式使用比較函式來搜索或排序未指定型別的陣列。如果宣告為的引數
size_t nmemb指定函式陣列的長度,則nmemb在呼叫該函式時可以將值設為零;不呼叫比較函式,搜索未找到匹配元素,排序不執行重新排列。此類呼叫的指標引數仍應具有有效值,如 7.1.4 中所述。
uj5u.com熱心網友回復:
從 POSIX 標準(強調是我的):
[CX]此參考頁面上描述的功能符合 ISO C 標準。此處描述的要求與 ISO C 標準之間的任何沖突都是無意的。本卷 IEEE Std 1003.1-2001 遵循 ISO C 標準。
該
qsort()函式將對一個物件陣列進行排序nel,其初始元素由 指向base。每個物件的大小(以位元組為單位)由width引數指定。如果引數的值為零,則不應呼叫nel指向的比較函式,compar也不應進行重新排列。
uj5u.com熱心網友回復:
正如其他人所提到的,最新版本的 C 標準以及 POSIX 明確允許nmemb引數為 0。但是,C89 標準中缺少這種語言。
C89的第 4.10.5 節(相當于 C90 的第 7.10.5 節)不包含規范之前的附加段落bsearch或qsort允許這樣做。因此,在嚴格的C89 模式下編譯可能會生成問題中的有效代碼。
C89 模式下的最新 gcc 未顯示違規行為:
https://godbolt.org/z/YhKoGEre7
但可以想象其他版本可以。我還沒有全部檢查。
uj5u.com熱心網友回復:
正如其他人所提到的,需要 C 標準庫函式qsort來正確處理零大小。
但這是從程式員的角度來看的。C 標準沒有規定任何關于生成的機器代碼的內容,只是它應該按照它應該的方式運行。
C 編譯器生成一個呼叫排序函式的二進制檔案是完全有效的,該排序函式不能正確處理 0 的大小,只要它在它之前添加一個零檢查。但是如果大小為零,我在 C89 標準中找不到任何允許 UB 的內容。
實際上,規范中的附加文本并沒有增加太多。相關部分是這樣的:
nmemb在呼叫該函式時可以將值設為零;不呼叫比較函式
這意味著這個片段:
#include <stdio.h>
#include <stdlib.h>
int cmpfunc (const void * a, const void * b) {
puts("foobar"); // To see if this function is executed
return ( *(int*)a - *(int*)b );
}
int main (void) {
int values[1] = {42};
qsort(values, 0, sizeof *values, cmpfunc);
}
如果您使用 C99 或更高版本進行編譯,則保證不會列印“foobar”。但是如果你用 C89 編譯,它可能會發生。或不。但是此代碼不會在 C89 或更高版本中呼叫未定義的行為。
John Bollinger 在評論部分提出了一個有趣的觀點
如果沒有明確說明第二個引數可能是 0,我可以為它做一個引數是 UB。它將圍繞這樣一個事實,即第二個引數必須是指向第一個引數的陣列的長度,并且 C 沒有零長度陣列。但是我仍然希望每個 C 實作都能以規范的后續版本所描述的自然方式處理這種情況。
在沒有明確要求允許大小為零的情況下,有一點回旋余地來解釋它是 UB。但是,C 標準明確將許多東西宣告為 UB,但不是這個。
我個人的意見(我很想知道在這個問題上是否有任何官方共識)是,如果規范是模糊的,但沒有明確說明為 UB,那么編譯器不應該使用歧義進行優化。
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/520268.html
上一篇:處理OS函式名稱與宏不匹配
