A couple of fun and intriguing C++ code snippets I explored while developing a Vulkan trace tool at ARM.
Compile-Time Type-to-Type Map
While working on translating the Vulkan API into our own data structure, I developed a compile-time type-to-type map. This map allows us to retrieve the corresponding CreateInfo type for a given Vulkan object type. Here’s the implementation:
template <typename... Elems> structTypeMap : Element<Elems>... { using Element<Elems>::Value...;
template <typename K> structFindHelper { using Type = typenamedecltype(TypeMap::Value(TypeTag<K>{}))::Type; };
template <typename K> using Find = typename FindHelper<K>::Type; };
// Key types structTypeA {}; structTypeB {};
// Value types structInfoA1 {}; structInfoA2 {}; structInfoB {};
// Create a TypeMap with types using TypeToInfoMap = TypeMap<Pair<TypeA, InfoA1, InfoA2>, Pair<TypeB, InfoB>>;
// Helper to get the type at a specific index in a tuple template <typename Tuple, std::size_t Index> using TupleElementType = std::tuple_element_t<Index, Tuple>;
intmain(){ using ValueA = TypeToInfoMap::Find<TypeA>; static_assert(std::is_same_v<ValueA, std::tuple<InfoA1, InfoA2>>, "Type mismatch!");
using ValueB = TypeToInfoMap::Find<TypeB>; static_assert(std::is_same_v<ValueB, std::tuple<InfoB>>, "Type mismatch!");
using FirstTypeValueA = std::tuple_element_t<0, ValueA>; static_assert(std::is_same_v<FirstTypeValueA, InfoA1>, "Type mismatch!");
using SecondTypeValueA = TupleElementType<ValueA, 1>; static_assert(std::is_same_v<SecondTypeValueA, InfoA2>, "Type mismatch!");
Here’s an odd case where passing an lvalue to a function expecting an rvalue doesn’t always cause a compiler complaint, depending on how it’s used. This snippet illustrates how you might unintentionally bypass this check:
intmain(){ auto sp_base = std::make_shared<base>(); auto sp_derived = std::make_shared<derived>();
// func(sp_base); // This line will cause a compile-time error func(sp_derived); // This line works func(std::move(sp_base)); // This line works func(std::move(sp_derived)); // This line works
return0; }
In this example, the function func expects an rvalue (std::shared_ptr<base>&&). Attempting to pass an lvalue directly to this function results in a compile-time error, as expected. However, when passing sp_derived, it works without a complaint because std::shared_ptr<derived> can be implicitly converted to std::shared_ptr<base>, even though it’s not an rvalue. Using std::move explicitly converts lvalues to rvalues, allowing the function to work as intended.
Managing Vulkan Handles Across Architectures: A Unified Approach
Vulkan, the low-overhead, cross-platform API for high-performance 3D graphics, uses handles to represent devices, queues, and other entities. These handles come in two flavors: dispatchable and non-dispatchable.
Handle Types in Vulkan
Dispatchable Handles
Dispatchable handles are pointers to opaque types and are used as the first parameter in API commands, allowing layers to intercept and process API calls. Each dispatchable object must have a unique handle value during its lifetime. On both 32-bit and 64-bit systems, these handles are represented as pointers to a struct, such as struct VkDevice_T *.
Non-Dispatchable Handles
Non-dispatchable handles are 64-bit integer types whose values can be implementation-dependent. If the privateData feature is enabled for a VkDevice, each non-dispatchable handle must be unique during its lifetime on that device. Otherwise, these handles can encode object information directly, potentially leading to non-unique values. This encoding is a performance optimization, enabling direct usage without dereferencing or indirection, even on 32-bit operating systems. Thus, 32-bit systems use uint64_t to represent non-dispatchable handles.
Problem
We can’t use void* to represent all handles. On 32-bit systems, non-dispatchable handles are uint64_t values, which cannot be directly cast to void* without causing errors. We need a unified approach to manage the handles on both 32-bit and 64-bit systems.
Solution
To address the differences in handle representations, we can use C++17’s std::variant to create a unified VkHandle type that can hold either a void* or a uint64_t. This approach ensures that our code remains portable and handles Vulkan objects correctly across different architectures.
Implementation
Here’s a step-by-step implementation of the solution:
Define the Unified Handle Type
1
using VkHandle = std::variant<void*, uint64_t>;
Define Hashing and Equality for VkHandle
Actually we can use the default hash and equality functions provided for std::variant.
// Create a map to associate VkHandles with integers std::unordered_map<VkHandle, int> handle_map; handle_map[non_dispatchable_handle] = 1; handle_map[dispatchable_handle] = 2;
// Demonstrate the function handling both types of handles DemoFunction(non_dispatchable_handle); // Output: 42 DemoFunction(dispatchable_handle); // Output: 0 (nullptr)
// Output the values stored in the map for each handle std::cout << handle_map[non_dispatchable_handle] << std::endl; // Output: 1 std::cout << handle_map[dispatchable_handle] << std::endl; // Output: 2
return0; }
Extract Vulkan Raw Handle from VkHandle
We can easily construct VkHandle from a Vulkan handle, but extracting the raw handle requires a bit more work. Here’s how we can do it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
template <classT> T Handle(const VkHandle &handle){ ifconstexpr(std::is_same<T, uint64_t>::value){ if (std::holds_alternative<uint64_t>(handle)) { // For non-dispatchable handle on 32-bit systems returnreinterpret_cast<T>(std::get<uint64_t>(handle)); } } else { if (std::holds_alternative<void *>(handle)) { returnreinterpret_cast<T>(std::get<void *>(handle)); } } return VK_NULL_HANDLE; }
To print the address of the handle, whether it’s a Vulkan raw handle or our VkHandle, we can use a unified method:
1 2 3 4 5 6 7 8 9 10 11 12
std::string HandleToHexString(const std::variant<void*, uint64_t>& vkhandle){ std::stringstream ss; if (std::holds_alternative<uint64_t>(vkhandle)) { uint64_t value = std::get<uint64_t>(vkhandle); ss << "0x" << std::hex << value; } else { void* value = std::get<void*>(vkhandle); ss << "0x" << std::hex << reinterpret_cast<uintptr_t>(value); } return ss.str(); }
If the input is a pointer to a struct, it will be converted to void* before being passed into the std::variant. Otherwise, the VkHandle will be passed or constructed in directly.
Flexible Type Deduction with is_constructible_v
I use std::conditional_t and std::is_constructible_v to automatically select the appropriate struct based on the argument types provided, while std::enable_if_t is used to constrain the template function definition.