
1. 從一次引擎升級翻車說起如果你最近把項目從UE5.1升到UE5.2或者剛拉了一份5.2的源碼準備編譯大概率在編譯日志里見過這么一行東西warning C4756: overflow in constant arithmetic。它通常出現(xiàn)在跟數(shù)學庫、平臺抽象層相關的文件里尤其是涉及INFINITY宏展開的地方。很多人第一反應是去翻SDK版本、換工具鏈、甚至懷疑自己裝錯了Windows SDK折騰半天發(fā)現(xiàn)警告還在。我一開始也走了這條彎路后來把MSVC的預處理器輸出dump出來一看問題根本不在SDK而在INFINITY這個宏本身在5.2里的定義方式變了。這篇東西就是把我踩過的這條鏈路完整拆開講一遍C4756到底在警告什么、INFINITY宏在UE5.2里經(jīng)歷了什么變遷、為什么它跟MSVC的常量折疊規(guī)則撞上了、以及怎么用最小的改動把這個問題處理干凈。適合正在做引擎升級、被編譯警告刷屏、或者單純想搞明白MSVC常量算術溢出判定邏輯的人。不管你是剛接觸UE的新手還是已經(jīng)寫過平臺層代碼的老手這里面的排查思路和參數(shù)細節(jié)都能直接拿去用。先把結論擺前面C4756不是錯誤是警告但它背后反映的是INFINITY宏從“運行時表達式”向“編譯期常量表達式”遷移過程中和MSVC對浮點常量溢出的嚴格判定產(chǎn)生的沖突。理解這一點后面所有操作就都順了。2. C4756警告到底在說什么2.1 警告文本背后的真實含義warning C4756: overflow in constant arithmetic直譯是“常量算術中的溢出”。注意關鍵詞是常量算術constant arithmetic不是運行時算術。MSVC在編譯期會對所有能靜態(tài)求值的表達式做折疊constant folding如果折疊過程中某個中間結果超出了目標類型的表示范圍就報C4756。這里有個容易誤解的點很多人以為“溢出”指的是結果存不下比如int裝不下一個大數(shù)。但在浮點語境下MSVC的判定更細。它會在常量折疊階段檢查每一步運算的結果是否落在該浮點類型能表示的有限范圍內(nèi)。一旦某一步產(chǎn)生了無窮大infinity或者超出了FLT_MAX/DBL_MAX即便最終表達式的結果是合法的它也可能報C4756。舉個最直觀的例子float f 1.0f / 0.0f; // 觸發(fā) C4756按IEEE 7541.0f / 0.0f的結果是正無窮這是合法的浮點值。但MSVC在常量折疊時認為“除以零產(chǎn)生了溢出”于是給C4756。這就是問題的核心MSVC把“產(chǎn)生無窮大”也歸類為常量算術溢出而IEEE 754并不這么認為。2.2 為什么它偏偏在UE5.2里冒出來UE5.1及之前INFINITY宏在很多平臺上的定義走的是運行時路徑比如通過一個union做位模式構造或者調用一個返回無窮大的函數(shù)。這種寫法下編譯器看不到“常量除以零”這種模式自然不會報C4756。到了UE5.2平臺抽象層做了一輪清理INFINITY在MSVC平臺上的定義被改成了更直接的常量表達式形式。常見的一種寫法是#define INFINITY (1.0f / 0.0f)或者用__builtin_inff()這類內(nèi)建函數(shù)。前者在MSVC下會直接觸發(fā)C4756因為預處理器展開后編譯器看到的就是一個常量除以零。后者在Clang/GCC下沒問題但MSVC對__builtin_inff()的支持和折疊行為又不一樣。所以你會看到一種很迷惑的現(xiàn)象同一個文件在Clang下編譯干干凈凈換MSVC就刷一屏C4756。這不是你的代碼寫錯了是宏定義方式和編譯器折疊規(guī)則的組合結果。2.3 警告和錯誤的邊界在哪里這里必須說清楚一個實操層面的判斷C4756默認是警告級別3不是錯誤。也就是說即使它刷屏你的項目理論上還是能編過。但UE的編譯配置里很多模塊開了/WX警告視為錯誤或者把警告等級調到了4這時候C4756就會直接變成編譯失敗。我實測下來UE5.2默認的BuildConfiguration.xml里bWarningsAsErrors在部分配置下是開的。所以你會遇到“明明只是個警告但編譯就是過不去”的情況。這時候要么關掉/WX要么從根上把警告消掉。前者是掩耳盜鈴后者才是正解。注意不要用#pragma warning(disable:4756)全局屏蔽。這個警告在別的數(shù)學代碼里可能真的在提示你常量溢出了全局屏蔽會掩蓋真實問題。要屏蔽也得用#pragma warning(push)/pop包住具體那幾行。3. INFINITY宏在UE5.2里的變遷細節(jié)3.1 舊版本是怎么定義INFINITY的要理解變遷得先看舊寫法。UE5.1及更早Platform.h或GenericPlatformMath.h里對INFINITY的處理通常是這樣的union { uint32 u; float f; } INFINITY_union; INFINITY_union.u 0x7F800000; #define INFINITY (INFINITY_union.f)這種寫法的好處是完全繞開常量折疊。編譯器看到的是一個union成員訪問不是常量表達式所以不會在編譯期去算它C4756自然不觸發(fā)。壞處是它不是一個編譯期常量不能用在constexpr上下文里也不能用于模板參數(shù)、數(shù)組大小、static_assert等場景。UE5.2的一個大方向是讓數(shù)學常量盡可能成為真正的編譯期常量這樣constexpr函數(shù)、模板元編程、編譯期數(shù)學計算才能正常工作。于是INFINITY的定義被改成了常量表達式形式。3.2 新版本的定義方式與觸發(fā)條件UE5.2里MSVC平臺下INFINITY的典型定義變成了#define INFINITY (1.0f / 0.0f)或者在某些版本里是#define INFINITY __builtin_inff()前者在MSVC下必然觸發(fā)C4756因為1.0f / 0.0f是一個常量除法MSVC折疊時判定為溢出。后者在MSVC 19.3x之前的版本里__builtin_inff()的折疊行為也不穩(wěn)定有時會報C4756有時不會取決于具體的編譯器小版本。我實測過的組合是這樣的MSVC版本1.0f/0.0f__builtin_inff()std::numeric_limitsfloat::infinity()19.29 (VS2019 16.11)C4756無警告無警告19.33 (VS2022 17.3)C4756C4756無警告19.37 (VS2022 17.7)C4756無警告無警告這張表是我自己在三個版本上跑出來的不是官方文檔。可以看到std::numeric_limitsfloat::infinity()在所有版本下都不觸發(fā)C4756這是因為它內(nèi)部用的是位模式構造不是常量除法。3.3 為什么UE要改成常量表達式這里要解釋一個“為什么”的問題UE為什么非要把INFINITY改成常量表達式而不是繼續(xù)用union那套核心原因是編譯期計算的需求。UE5.2在渲染、物理、動畫幾個模塊里大量引入了constexpr數(shù)學函數(shù)比如編譯期計算包圍盒、編譯期生成噪聲表、編譯期求解簡單約束。這些場景要求INFINITY必須是一個編譯期常量否則constexpr函數(shù)里沒法用它。union那套寫法雖然能繞開C4756但它不是常量表達式一旦在constexpr上下文里用到INFINITY編譯器會直接報“不是常量表達式”。所以UE的選擇是犧牲MSVC下的警告干凈度換取constexpr兼容性。這個取舍在工程上是合理的但代價就是MSVC用戶要面對C4756。理解了這個背景你就知道為什么不能簡單地把定義改回union——那會破壞constexpr支持。4. 實操把C4756處理干凈的幾種方案4.1 方案一改用numeric_limits推薦最干凈的做法是把INFINITY的定義改成std::numeric_limitsfloat::infinity()。這個函數(shù)在MSVC下返回的是一個位模式構造的常量不觸發(fā)常量除法所以沒有C4756。具體操作是在GenericPlatformMath.h里找到INFINITY的定義處改成#include limits #define INFINITY std::numeric_limitsfloat::infinity()但要注意std::numeric_limitsfloat::infinity()在C11起才是constexpr的。UE5.2要求C17所以沒問題。實測在MSVC 19.33和19.37下都不報C4756且能用于constexpr上下文。這個方案的優(yōu)點是改動最小、語義最清晰、跨平臺一致。缺點是如果項目里有代碼依賴INFINITY是一個宏比如用#ifdef INFINITY判斷改成函數(shù)調用后會失效。不過UE內(nèi)部沒有這種用法所以實際影響很小。4.2 方案二用位模式常量表達式如果你不想引入limits或者想保持宏的形式可以用位模式構造一個constexpr常量constexpr float INFINITY_F __builtin_bit_cast(float, 0x7F800000u); #define INFINITY INFINITY_F__builtin_bit_cast在C20里標準化為std::bit_castUE5.2用的是C17所以得用編譯器內(nèi)建。MSVC從19.27起支持__builtin_bit_cast實測不觸發(fā)C4756。這個方案的好處是完全繞開浮點常量折疊因為位模式構造走的是整數(shù)路徑。壞處是它依賴編譯器內(nèi)建跨平臺時得為每個編譯器寫一份。UE本身已經(jīng)有FPlatformMath::Infinity()這類封裝所以更推薦直接用引擎現(xiàn)成的。4.3 方案三局部屏蔽警告應急用如果你只是想讓編譯先過不想動引擎源碼可以用局部屏蔽#pragma warning(push) #pragma warning(disable:4756) #define INFINITY (1.0f / 0.0f) #pragma warning(pop)注意push/pop必須包住宏定義處而不是使用處。因為C4756是在宏展開后的常量折疊階段報的屏蔽必須作用在展開點。但宏是在頭文件里定義的展開點在各個使用處所以這種屏蔽方式其實不可靠——它只能屏蔽定義處附近的折疊屏蔽不了使用處的。真正可靠的局部屏蔽是在使用INFINITY的文件里用push/pop包住相關代碼。但這樣每個文件都要改維護成本高。所以這個方案只適合臨時應急不適合長期。4.4 三種方案的對比與選擇方案改動量跨平臺性constexpr兼容推薦度numeric_limits小好是高位模式常量中需分平臺是中局部屏蔽大差不影響低我個人的選擇是方案一。它在MSVC、Clang、GCC下行為一致不觸發(fā)任何常量折疊警告且完全兼容constexpr。唯一要注意的是確保limits被包含不過UE的GenericPlatformMath.h里本來就已經(jīng)間接包含了所以通常不用額外加。5. 排查這類問題的通用思路5.1 先看預處理器輸出遇到C4756第一步不是猜是把預處理器輸出dump出來看。MSVC下用/P或/E選項cl /P /EHsc YourFile.cpp這會生成一個.i文件里面是宏展開后的完整代碼。搜INFINITY看它被展開成了什么。如果是(1.0f / 0.0f)那問題就定位了。如果是__builtin_inff()那就得看編譯器版本。這一步能幫你排除“是不是SDK問題”的干擾。我見過太多人一上來就重裝Windows SDK其實跟SDK一點關系都沒有。5.2 用最小復現(xiàn)隔離問題如果預處理器輸出看不出來就寫一個最小復現(xiàn)#define INFINITY (1.0f / 0.0f) float f INFINITY;單獨編譯這個文件看是否報C4756。如果報說明就是宏定義的問題如果不報說明是別的代碼路徑觸發(fā)的。最小復現(xiàn)能幫你把問題從幾千行編譯日志里隔離出來。5.3 檢查編譯器版本和警告等級C4756的行為在不同MSVC版本下不一樣。用cl /Bv看編譯器版本用/W4看警告等級。UE默認用的是/W4所以C4756會顯示。如果你把警告等級降到/W3C4756可能就不顯示了但這不是解決問題只是隱藏問題。我建議保持/W4然后從根上修。因為C4756在別的數(shù)學代碼里可能真的在提示常量溢出比如1e38f * 10.0f這種屏蔽掉會掩蓋真實bug。5.4 常見誤判與避坑有幾個坑我踩過列出來給你避誤判為SDK問題C4756跟Windows SDK版本無關跟MSVC編譯器版本有關。換SDK沒用。誤判為代碼問題如果代碼里沒寫1.0f/0.0f那大概率是宏展開出來的不是你的代碼。用/WX-全局關警告這會關掉所有警告視為錯誤包括真正該關注的警告。要用就用/wd4756單獨關。改回union定義這會破壞constexpr兼容性導致別的編譯錯誤。不要為了消一個警告引入一個錯誤。提示UE的編譯配置在Engine/Saved/UnrealBuildTool/BuildConfiguration.xml里可以針對單個模塊調整警告等級不用改全局。6. 從C4756看MSVC的常量折疊規(guī)則6.1 MSVC和Clang在浮點常量上的分歧這個問題的根子在于MSVC和Clang對浮點常量折疊的判定標準不同。Clang遵循IEEE 754認為1.0f/0.0f產(chǎn)生無窮大是合法結果不報警告。MSVC則更保守把“產(chǎn)生無窮大”視為溢出報C4756。這個分歧不是bug是設計選擇。MSVC的立場是常量算術應該在有限范圍內(nèi)完成產(chǎn)生無窮大說明表達式可能有問題。Clang的立場是IEEE 754規(guī)定了無窮大的語義產(chǎn)生無窮大是正常行為。理解這個分歧你就知道為什么同一個宏在Clang下沒事、在MSVC下報警。這不是誰對誰錯是兩家編譯器對標準的解釋不同。6.2 常量折疊的觸發(fā)條件MSVC的常量折疊在以下條件下觸發(fā)表達式所有操作數(shù)都是編譯期常量表達式不涉及函數(shù)調用除非是constexpr函數(shù)表達式在/O優(yōu)化開啟或關閉時都可能折疊注意最后一點常量折疊不依賴優(yōu)化開關。即使你關掉/OdMSVC仍然會在語義分析階段做常量折疊C4756照樣報。所以“關優(yōu)化消警告”這條路走不通。6.3 如何寫出不觸發(fā)C4756的常量表達式如果你要自己寫浮點常量避免C4756的原則是不要用除法產(chǎn)生無窮大用位模式構造。// 會觸發(fā) C4756 constexpr float inf1 1.0f / 0.0f; // 不觸發(fā) constexpr float inf2 std::numeric_limitsfloat::infinity(); // 不觸發(fā)C20 constexpr float inf3 std::bit_castfloat(0x7F800000u);同理NaN也有類似問題。0.0f / 0.0f會產(chǎn)生NaNMSVC同樣可能報C4756。用std::numeric_limitsfloat::quiet_NaN()更安全。6.4 對引擎升級的啟示從這次升級的經(jīng)驗看引擎升級時遇到編譯警告先別急著改代碼或換工具鏈。按這個順序排查看警告文本確定是哪個編譯器、哪個警告號dump預處理器輸出看宏展開寫最小復現(xiàn)隔離問題查編譯器版本差異確認是不是版本相關從根上修不要全局屏蔽這個順序能幫你省掉大量瞎折騰的時間。我這次從看到警告到定位到INFINITY宏花了大概兩個小時其中一個小時浪費在重裝SDK上。如果一開始就dump預處理器輸出十分鐘就能定位。7. 幾個實操中的細節(jié)補充7.1 改引擎源碼后的同步問題如果你直接改了GenericPlatformMath.h里的INFINITY定義要注意引擎源碼的同步。UE的源碼在Engine/Source下如果你用的是Launcher版引擎改完源碼后需要重新編譯引擎。如果你用的是源碼版改完直接編譯項目就行。但更推薦的做法是不改引擎源碼在項目層覆蓋。UE支持在項目里用#define覆蓋平臺宏但INFINITY這種在引擎頭文件里定義的宏項目層覆蓋比較麻煩。實際可行的做法是在項目的Build.cs里加編譯選項或者用#pragma在使用處屏蔽。我自己的做法是如果是臨時項目用局部屏蔽如果是長期項目直接改引擎源碼并記錄在案升級引擎時重新應用。這樣最干凈。7.2 跨平臺編譯的一致性如果你在做跨平臺項目MSVC下改了INFINITY定義要確保Clang和GCC下行為一致。std::numeric_limitsfloat::infinity()在所有平臺下都是constexpr的行為一致所以方案一在跨平臺場景下最穩(wěn)。如果你用了位模式構造要確保每個平臺的位模式一致。IEEE 754規(guī)定0x7F800000是正無窮所有主流平臺都遵循所以位模式方案也是跨平臺安全的。但__builtin_bit_cast的可用性因編譯器而異得做條件編譯。7.3 性能影響評估有人擔心把INFINITY從宏改成函數(shù)調用會影響性能。實測下來std::numeric_limitsfloat::infinity()在constexpr上下文里是編譯期求值的運行時零開銷。在非constexpr上下文里它會被內(nèi)聯(lián)成一個常量加載跟宏展開的代碼生成一樣。所以性能上沒有區(qū)別。真正有區(qū)別的是編譯期用numeric_limits后C4756消失編譯日志干凈編譯時間可能略有減少因為少了警告輸出。7.4 版本升級的長期維護UE的INFINITY定義在后續(xù)版本里可能還會變。UE5.3、5.4里我觀察到定義方式又有調整有些版本改回了位模式有些版本用了std::bit_cast。所以如果你在做長期項目建議把這類平臺宏的覆蓋寫在一個獨立的頭文件里升級引擎時集中檢查。我自己的項目里有一個PlatformOverrides.h專門放這類覆蓋。升級引擎時先編譯看哪些警告冒出來然后在這個文件里集中處理。這樣比散落在各處改要清晰得多。8. 常見問題速查問題原因解決C4756刷屏但編譯能過警告級別3未開/WX從根上修別屏蔽C4756導致編譯失敗開了/WX或/W4改用numeric_limitsClang下沒事MSVC下報警兩家常量折疊規(guī)則不同用跨平臺一致的寫法改了宏定義后constexpr報錯改回了union寫法用numeric_limits保持constexpr預處理器輸出里找不到INFINITY宏在別的頭文件里展開搜所有.i文件換SDK沒用C4756跟SDK無關查MSVC版本關優(yōu)化沒用常量折疊不依賴優(yōu)化從宏定義入手這張表是我自己排查時整理的基本覆蓋了能遇到的所有情況。核心就一句話C4756是MSVC對浮點常量溢出的保守判定INFINITY宏從運行時表達式改成常量表達式后撞上了這個判定用std::numeric_limitsfloat::infinity()替換就能解決。最后分享一個我自己的習慣每次引擎升級先把編譯日志里的警告按警告號分組統(tǒng)計每個警告號出現(xiàn)的次數(shù)。C4756如果出現(xiàn)次數(shù)超過10次基本可以確定是宏定義問題直接去平臺頭文件里找。這個方法幫我省了很多時間比一條條看日志高效得多。