I’m going to say it. If you are a C++ developer and learning or using Vulkan professionally, I believe you should be using the vulkan.hpp header. Most of the examples I see in the wild use the C interface, and I get why this is the case. We’re used to seeing an OpenGL-esque C-style interface and its also the primary source of all the documentation. My hope is that things will settle on the C++ version over time on all fronts: documentation, examples, and real-world code. Here are just a few of the benefits:

  • Type-safe enums
  • No need to set the overly verbose VK_STRUCTURE_TYPE_* enums on the sType fields
  • Clear return-value interface for various API calls (as opposed to opaquely modifying a passed pointer’s memory)
  • Idiomatic initializer-list style argument passing for variadic fields (compared to size and pointer)
  • Optional RAII based handles which delete the underlying resource when destroyed (terrifyingly convenient at a small cost, but you needn’t use it if you don’t want to)

The importance of these affordances should not be understated. If you are already a C++ developer, you know the importance of leaning on the compiler to tell you what’s wrong (again, I’m assuming that the reader of this article is already in the C++ camp and I don’t want to fight the C vs C++ battle here). For a very explicit (read. verbose) API, the surface area of mistakes, typos, and more is vast. Minimizing this will save countless hours and headache in the long run. Secondary benefits which arise as a consequence of the above are:

  • Better usage of Intellisense or something similar (since enums are strongly typed, you can narrow the field of options much more quickly)
  • You can build simpler abstractions around Vulkan resources since you won’t need as many non-trivial destructors (and move constructors, and move assignment operators)
  • The time you save typing and checking mindless boilerplate can be put towards solving actual problems

One of the larger impediments to using vulkan.hpp is the perceived risk of doing so. After all, most of the books, tutorials, and examples use the C API, so why should I? I felt similarly some time ago, but no longer believe this is the case for the following reasons:

  • The abstraction layer is thin enough that after a few moments, translating from the C++ API to the C API is trivial. In fact, the C++ code is auto-generated.
  • The sizeof each wrapping C++ class is statically asserted to be the same as the underlying C type. You really aren’t paying for anything when you switch.
  • Similarly, the type safe enums in the C++ API match the C enums exactly.
  • For functions that require error checking, the header already does the hard work for you, and you can opt for the API with exceptions enabled, or with error return codes.

So if you really really wanted to, you could even intermix C and C++ easily (most of it is just POD), but you’ll find that you simply don’t need to, and after making what I believe is an easy mechanical transition, you’ll likely never look back.

Some tips on usage:

  • Read this but do read the README over at the repo. It’s not that long and is a superset of this short list here (this list is just a summary to whet your appetite)
  • All Vk* structs have a corresponding C++ type which is just vk::*. For example, VkImage gets translated to vk::Image.
  • All VK_*_* enums have a corresponding enum class which looks like vk::*::* (enums are split into type and value). For example, VK_IMAGE_TYPE_2D becomes vk::ImageType::e2D. Note the e prefix of the enum class value. Also note that everything becomes Pascal cased again.
  • Function calls of the form vk*(*, ...) become member function calls of the target resource, often in the first parameter. For example vkBindBufferMemroy(device, ...) translates to device.bindBufferMemory(...).
  • Enum classes are not generally treatable as bitmasks (they don’t support the various bitwise operators), so to get around this, a wrapping vk::Flags<T> class is provided for each bitmask. You don’t often actually need to care. Just know that in places where a bitwise | may be needed to set certain bits, you can just do something like vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eA without issue. Most flag values have FlagBits as part of the name and the wrapping templated class ends in Flags.
  • Use initializer lists to construct things more tersely when arguments would have normally taken a count and pointer.
  • Whether you use exceptions or error codes is up to you. The exceptions enabled code is a bit more concise and reads better since you don’t need to wrap return values, but your codebase may not allow exceptions (or you may be allergic to them). Personally, I use the exceptions-enabled interface because most of the exceptions I would encounter running Vulkan are fatal anyways.
  • Almost every vk::* resource has a corresponding vk::Unique* resource handle. It’s just like std::unique_ptr! Use the * or -> to access its contents, and when it gets destroyed, the undlerying resource (for example, vk::UniqueBuffer) will get destroyed with it. Note that unlike everything else mentioned thus far, there is more of a cost to using a vk::Unique* handle since often a deleter needs to be stored. Do I think it’s important? Most likely not. What needs to be stored are things like the pool a thing was allocated from (in which case a PoolFree is invoked transparently) or a reference to the parent object (e.g. the Device)
  • For VkBool32 types, feel free to continue to use VK_TRUE and VK_FALSE

When using the C++ bindings, standard C++ style conventions apply. Write abstractions that have non-trivial destructors wherever possible, and wrap individual resources so that they can be composed in default-movable classes later. For functions and constructors with a lot of parameters (Vulkan is very configurable after all), try a newline-delimited comments-included convention like so:

// Excerpts from my own code

m_color = vk::PipelineColorBlendAttachmentState{
    VK_TRUE,                            // Blend enable
    vk::BlendFactor::eSrcAlpha,         // Source color blend factor
    vk::BlendFactor::eOneMinusSrcAlpha, // Dst color blend factor
    vk::BlendOp::eAdd,                  // Color blend op
    vk::BlendFactor::eOne,              // Source alpha blend factor
    vk::BlendFactor::eZero,             // Dst alpha blend factor
    vk::BlendOp::eAdd,                  // Alpha blend op
    vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
        vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA // Color write mask
};

m_blend = vk::PipelineColorBlendStateCreateInfo{
    vk::PipelineColorBlendStateCreateFlags{}, // Flags
    VK_FALSE,                                 // Logic op enable
    vk::LogicOp::eClear,                      // Logic op
    1,                                        // Attachment count
    &m_color,                                 // Attachments
    {0.0f, 0.0f, 0.0f, 0.0f}                  // Blend constants
};

In summary, if you haven’t tried out the C++ bindings yet, go for it! Your fingers will thank you.