在專業軟件開發中,最重要的一個方面就是盡早發現錯誤。當然,最好的情況是我們甚至不能編寫錯誤的代碼。其次最好的是編譯器可以檢測到的錯誤。

最壞的情況是運行時錯誤。最難的部分隱藏在只在特定情況下運行的代碼中。墨菲定律說,這種情況首次發生在顧客的環境中。
如果您使用的是 Vulkan ,有幾種方法可以創建運行時錯誤。即使 Vulkan 提供了很好的驗證層,您也必須運行這部分代碼來檢測此類錯誤。順便說一句,我建議你不要在沒有使用驗證層的情況下用 Vulkan 編程!
當使用 Vulkan – hpp 時,一些運行時錯誤變成編譯時錯誤 . Vulkan -HPP 是針對 Vulkan API 的頭報頭 C ++綁定。它由 Khronos 維護,作為 Vulkan 生態系統的一部分,可以在 GitHub 上找到 Khronos Group / Vulkan – hpp 。它也是 LunargVulkan SDK 的一部分。有關詳細信息,請參閱 Vulkan C ++綁定加載 和 開放源碼 Vulkan C ++ API 。
有助于將錯誤轉移到編譯時的特性
Vulkan -hpp 通過以下功能幫助消除運行時錯誤:
- 枚舉類與普通枚舉比較
- 幫助程序類
vk::Flags
- 結構 常量成員
sType
vk::StructureChain
- 處理 32 位版本中的 類型安全
枚舉類
使用 Vulkan ,可以得到很多枚舉類型。除了 VkResult
,它們都是使用以下命名方案構造的:
typedef enum VkEnumName {
VK_ENUM_NAME_VALUE_A = 0,
VK_ENUM_NAME_VALUE_B = 1,
…
} VkEnumName;
使用 Vulkan -hpp ,可以為這些枚舉類型中的每一種獲得一個枚舉類:
namespace vk
{
…
enum class EnumName
{
eValueA = VK_ENUM_NAME_VALUE_A,
eValueB = VK_ENUM_NAME_VALUE_B,
…
};
…
}
首先,它們都位于名稱空間 vk
。您可以通過定義 VULKAN_HPP_NAMESPACE
來調整該命名空間。 enum 類本身沒有前綴 Vk
,因為這對于命名空間來說是多余的。最后,一個 enum 類的每個值都跳過前綴 VK_ENUM_NAME
,因為這個前綴又與命名空間和枚舉類名冗余。它們以小寫字母“ e ”作為前綴,并包含實際枚舉值名稱的 camelCase 版本。枚舉類值不允許以數字開頭,因此“ e ”前綴阻止了這一點。例如,無論你在 C 代碼中使用 VK_ENUM_NAME_VALUE_A
,都使用 vk::EnumName::eValueA
代替 C ++代碼。
那么,你從 Vulkan -hpp 中的 enum 類得到了什么呢?畢竟,由于 Vulkan 中的枚舉值命名方案,不可能有兩個同名的枚舉值。這根本不是你的問題,而是 Khronos 的 Vulkan 人的問題。此外,您不太可能希望將變量或函數作為枚舉值之一命名,即使這些名稱已導出到全局范圍。誰知道呢?有人喜歡函數名,比如 MIG 。
這里重要的一點是,在 Vulkan -hpp 中,沒有隱式轉換到 int 。不能將枚舉類值賦給 int
類型,至少不會意外。也不能比較來自不同枚舉類的兩個枚舉類值。當您比較兩個枚舉器中的兩個枚舉值時,會產生警告。 MIG 是依賴于編譯器的,當然,一個警告比錯誤更容易被忽略。
助手類 vk :: Flags
對于 Vulkan ,有兩個數據類型對,使用以下命名方案:
typedef enum VkEnumNameFlagBits = {
VK_ENUM_NAME_VALUE_A_BIT = 0x00000001,
VK_ENUM_NAME_VALUE_B_BIT = 0x00000002,
…
} VkEnumNameFlagBits;
typedef VkFlags VkEnumNameFlags;
這里, VkFlags
只是一個 uint32_t
,并且 VkEnumNameFlags
應該通過從 VkEnumNameFlagBits
中對適當的枚舉值進行排序來保存相應枚舉 VkEnumNameFlagBits
的零個或多個值。由于* FlagBits
和* Flags
之間除了它們的公共名稱部分之外,沒有真正的聯系,編譯器對此無能為力。允許對任意枚舉值或整數應用位運算符。如果錯誤組合的* Flags
值恰好是位的有效組合,即使它們可能不是您所希望的那樣,即使驗證層 MIG ht 也無法捕捉到這一點。它 MIG 感覺你像是在未定義的行為領域,即使程序完全按照你告訴它做的去做。這不是你想讓它做的。
使用 Vulkan -hpp ,可以得到相應的對:
namespace vk
{
…
enum class EnumNameFlagBits : VkEnumNameFlagBits
{
eValueA = VK_ENUM_NAME_VALUE_A_BIT,
eValueB = VK_ENUM_NAME_VALUE_B_BIT,
…
};
using EnumNameFlags = Flags<EnumNameFlagBits>;
…
}
有了這個結構,這樣的尷尬局面就不會發生了。不能對枚舉類值應用位運算符。 vk::EnumNameFlags
枚舉知道相應的 vk::EnumNameFlagBits
。 helper 類 vk::Flags
提供的功能允許您對來自同一個枚舉類的枚舉類值應用位運算符,但僅對這些值應用這些值。不能將它們與來自不同枚舉類的值組合。在編譯時,只使用允許的標志位構造標志。
結構的 sType 成員
稍微遠離枚舉, Vulkan 中有許多結構將枚舉類型 VkStructureType
的成員 sType
作為第一個元素。對于這些結構中的每一個,都必須將該成員設置為為為該結構指定的值。在下面的代碼示例中,成員 sType
必須設置為 VK_STRUCTURE_TYPE_STRUCT_NAME
。
typedef struct VkStructName {
VkStructureType sType;
…
} VkStructName;
沒那么難,但你必須做對。不要忘記設置它,也不要通過從代碼中的另一個位置復制來將其設置為錯誤的值。例如,以下值在視覺上接近:
VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO
VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO
對于 Vulkan -hpp ,在結構上有以下限制:
namespace vk
{
…
struct StructName
{
…
const vk::StructureType sType = vk::StructureType::eStructName;
…
};
…
}
就這么簡單。當您實例化類型為 StructName
的結構時,您不必擔心為成員 sType
設置正確的值,因為它已經設置好了。因為它是常量成員,所以不能意外地覆蓋它。
對于感興趣的模板元程序員來說: struct StructName
還提供了一個靜態成員 structureType
,即 vk::StructureType
值。有一個名為 CppType
的類型特征,它從 vk::StructureType
值中獲取結構的類型。
幫助程序類 vk :: StructureChain
Vulkan 中的許多結構都有 pNext
作為第二個成員:
typedef struct VkStructName {
VkStructureType sType;
const void* pNext;
…
} VkStructName;
某些結構被指定為延伸其他結構。 pNext
指針用于創建結構鏈。有些結構可以多次成為該鏈的一部分,具有不同的實例。您的代碼 MIG ht 如下所示:
ChainedStruct chained = {};
chained.sType = VK_STRUCTURE_TYPE_CHAINED_STRUCT;
// set other values of chained
AnchorStruct anchor = {};
anchor.sType = VK_STRUCTURE_TYPE_ANCHOR_STRUCT;
anchor.pNext = &chained;
// set other values of anchor
使用這種方法,有幾個潛在的錯誤。例如, MIG ht 將一個結構鏈到 AnchorStruct
實例,該實例沒有指定在鏈中。或者您意外地鏈接了一個結構的多個實例,其中只允許一個實例。甚至內存管理也會導致意外行為。例如,當您有一個使用局部變量創建鏈的函數時,就像前面的代碼示例中那樣,當該函數最終按值返回錨點時,您已經注定要失敗了。 MIG 指出的鏈式結構已經消失。
對于 Vulkan -hpp ,該代碼看起來幾乎相同:
namespace vk
{
…
struct StructName
{
…
const vk::StructureType sType = vk::StructureType::eStructName;
const void * pNext = {};
…
}
…
}
您可以使用與 Vulkan 相同的方法來使用它,潛在錯誤的來源相同。 helper 類 vk::StructureChain
在這里幫助編譯器,如下面的代碼示例所示:
vk::StructureChain<vk::AnchorStruct, vk::ChainedStruct> chain
(
{ /* set other values of anchor */ }
{ /* set other values of chained */ }
);
當然,這條鏈會變得任意長。編譯器可以檢查是否所有鏈式結構都指定為擴展 MIG 。如果同一個鏈式結構多次出現,編譯器會檢查是否允許。訪問這樣一個鏈的元素將與您習慣于使用純 C 型結構鏈略有不同:
vk::AnchorStruct const & anchorStruct = chain.get<vk::AnchorStruct>();
vk::ChainedStruct const & chainedStruct = chain.get<vk::ChainedStruct>();
如果必須在運行時從結構鏈中刪除元素,可以使用成員函數 vk::StructureChain::unlink
來執行此操作。這樣,結構鏈的內存占用不會改變,但是現在未使用的部分將被跳過,因為該鏈中的任何 pNext
指針都不會指向這些部分。要重新鏈接,請使用 vk::StructureChain::relink
。
型式安全
最后,我們來看一個稍微不同的主題,類型安全。這是 Vulkan 的一個問題,尤其是對于 32 位構建。在 32 位內部版本中,所有不可分派的句柄(如 VkBuffer
、 VkImage
和 VkSemaphore
都只是 uint64_t
上的 typedef
:
#define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef uint64_t object; … VK_DEFINE_NON_DISPATCHABLE_HANDLE(VkBuffer) VK_DEFINE_NON_DISPATCHABLE_HANDLE(VkImage)
您可以調用 vkCreateBuffer
并傳入指向 VkImage
的指針作為最后一個參數。您還可以將 VkImage
分配給 VkBuffer
或對它們進行比較,沒有任何錯誤!
由于 Vulkan -hpp 中的相應類型是獨立的類,沒有任何繼承關系,編譯器不允許這些操作中的任何一個。你不會弄錯的。
與運行時錯誤相比,更傾向于編輯時錯誤預防
正如我前面所說,最好的情況是甚至不能編寫錯誤的代碼。 Vulkan -hpp ,可能與所有現代 IDE 一起幫助您設置結構的成員。因為每個 vk :: struct 都有一個構造函數,該構造函數的每個成員都有一個參數列表,除了前面提到的 sType
和 pNext
, IDE 可能會在編輯時引導您遍歷所有這些參數,從而更難忽略任何錯誤。
把您的 Vulkan 的項目切換到 C ++
這些是 Vulkan -hpp 的一些特性,它們極大地簡化了用 Vulkan 進行編碼的工作。這些特性在運行時開銷為零的情況下可用。只是編譯器需要多工作一點。您仍然必須(幾乎)像使用 plainVulkan 一樣顯式地編程,但是編譯器可以更好地檢查代碼。這樣可以節省你很多時間!