1

I'm trying to wrap VkInstance (opaque pointer) into unique_ptr but seems like I can't.

...
    VkInstance instance;
    if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
        throw std::runtime_error("failed to create vulkan instance");
    }

    auto del = [](VkInstance* p) {
        DBG("release vk instance");
        vkDestroyInstance(*p, nullptr);
    };
    auto ptr = std::unique_ptr<VkInstance, decltype(del)>(instance, del);
...

The error:

no instance of constructor "std::unique_ptr<_Tp, _Dp>::unique_ptr [with _Tp=VkInstance, _Dp=lambda [](VkInstance *p)->void]" matches the argument list

I can't figure out why. VkInstance is a pointer, so I'm passing it, deleter have to accept pointer to a memory address, so it receiving it still, but still types doesn't match.

Dereferrencing it with & and passing it into make_unique causes a segfault. Makes sense.

The only way I managed to make it work only with with additional new call, like this:

...
    VkInstance* instance = new VkInstance;
    if (vkCreateInstance(&createInfo, nullptr, instance) != VK_SUCCESS) {
        throw std::runtime_error("failed to create vulkan instance");
    }

    auto del = [](VkInstance* p) {
        DBG("release vk instance");
        vkDestroyInstance(*p, nullptr);
    };
    auto ptr = std::unique_ptr<VkInstance, decltype(del)>(instance, del);
...

But this is kinda ridiculous solution, as I'm allocation dynamically for a thing which should lay in a CPU register and transferred almost immediately to unique_ptr area of control.

So, can I achieve somehow what I'm trying to do, without further overengineering?

dbush
  • 205,898
  • 23
  • 218
  • 273
Artsiom Miksiuk
  • 3,896
  • 9
  • 33
  • 49
  • Did you try &instance with your std::unique_ptr version? – George Feb 20 '23 at 20:44
  • A `unique_ptr` manages a pointer to a value allocated elsewhere. If you don't want that then you can't use a `unique_ptr` – Kevin Feb 20 '23 at 20:45
  • @Kevin that's not strictly true. You can use a unique_ptr to perform some cleanup action (not necessarily a call to delete or delete[], since you can use a custom deleter) on some object you can get a pointer to, so long as the unique_ptr gets destroyed before the object it is supposedly managing does. What Artsiom is attempting to do is managing a resource, that resource just isn't memory allocated by new. – George Feb 20 '23 at 20:59
  • @George I mean that the `unique_ptr` doesn't hold the resource itself, but a pointer to it. So the resource has to be stored somewhere else. – Kevin Feb 20 '23 at 21:52
  • 1
    Vulkan instances are NOT C++ pointers. Those are rather handles generated by a driver which contain a value which may look like a pointer to a memory location but it is not. – Michael IV Feb 21 '23 at 08:13

4 Answers4

5

std::unique_ptr can work with types that aren't even pointers; it can certainly work with opaque pointer types like VkInstance. However, you have to know how to do things the way std::unique_ptr expects.

The key point is this: stop using lambdas for the deleter. unique_ptr's deleter type has useful functionality in it, none of which can be used if you use a lambda type. Also, if you want to use the name of the type of a functor, having to do decltype gymnastics instead of just giving it a name is kind of tacky.

So make a proper deleter:

struct VkInstanceDeleter
{
  using pointer = VkInstance;

  void operator()(VkInstance inst) {vkDestroyInstance(inst, nullptr);}
};

using InstPtr = std::unique_ptr<VkInstance, VkInstanceDeleter>;

InstPtr can now be used to manage VkInstance objects. The only issue is that you can't use -> on it, but that's not meaningful in Vulkan anyway.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • What's the matter with lambdas? I don't need to reuse deleter type anywhere, and I'm not sure which additional functionality you are referring to. I really just need replace delete function. Trying your solution, but still it can't be used. `error: invalid initialization of reference` – Artsiom Miksiuk Feb 21 '23 at 08:32
  • Oh, did you mean, that I need to do allocation into VkInstanceDeleter.pointer field? Then, I guess this would make a wrong abstraction, and I'm better go with symmetric create/delete wrapper. – Artsiom Miksiuk Feb 21 '23 at 10:33
  • 1
    @ArtsiomMiksiuk If you do not need the deleter anywhere else, I don't see what forbids you to define that `struct` in the scope where you need it. There's nothing to change in the allocation. You just put `struct VkInstanceDeleter{/*...*/};` at the place you had your `auto del = ...;`, and the next line is just `auto ptr = std::unique_ptr{instance};`. – Erel Feb 23 '23 at 15:24
  • @Erel On this part you and Nicol Bolas are right. But still looks like unique_ptr can't work with non pointer values. At least I can't make it to without additional dynamic allocations, which is a bummer. – Artsiom Miksiuk Feb 23 '23 at 17:00
  • 1
    @ArtsiomMiksiuk: It doesn't have to be a pointer specifically, but it *does* have to be a "NullablePointer" type. This [means (among other things) that you can assign `nullptr` to it and test it against `nullptr`](https://en.cppreference.com/w/cpp/named_req/NullablePointer). But [you can easily wrap any object you want in a type that you can give NullablePointer characteristics to](https://stackoverflow.com/a/15757618/734069). – Nicol Bolas Feb 23 '23 at 17:20
  • @ArtsiomMiksiuk Though I may be wrong, I think the underlying problem is that you misunderstand the nature of what you are working with. A [`VkInstance`](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkInstance.html) [is a pointer](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_DEFINE_HANDLE.html). It is has its own analogue to `new` / `malloc` (`vkCreateInstance`) and its own analogue to `delete` / `free`( `vkDestroyInstance`). `unique_ptr` does not care about allocation, only about deletion, thus the answer above. That's really all there is to it. – Erel Feb 23 '23 at 17:20
  • @Erel, I know that it is a pointer, but it is an opaque handler, and I guess I shouldn't treat it anyhow different except as a value to be passed back into Vk calls. Therefore, I need either wrap such type into own RAII class, or try to adapt it for `unique_ptr` calls, which Nicol pointed to me should be possible. – Artsiom Miksiuk Feb 23 '23 at 17:26
  • @ArtsiomMiksiuk: "*I need either wrap such type into own RAII class*" Personally, I remain unconvinced that RAII is an appropriate tool for managing Vulkan objects. While `VkInstance` has no requirements, basically every other kind of Vulkan object is created relative to some other object. Devices are created by an instance, and you cannot *destroy* that device without having that instance. Also, you cannot destroy the instance until you have destroyed all devices owned by that instance. My point is that the ownership dynamic is not amenable to RAII. – Nicol Bolas Feb 23 '23 at 17:29
  • @NicolBolas First of all, thanks, your solution worked. Idk what I did wrong before, but day before I couldn't make it work) Now it compiled and didn't segfault. That's a win. Regarding managing Vulkan objects. "you cannot destroy that device without having that instance", technically I can. VkDestroyDevice requires only logical device handler. Logically, how the program is build (in my universe) vkInstance init, P device init, L device init ... On destruction, with RAII, it would automatically do the right order destruction, which is a perfect fit I think. Or you meant something else? – Artsiom Miksiuk Feb 23 '23 at 17:40
  • @ArtsiomMiksiuk: I was wrong about `vkDestroyDevice` taking an instance, but commands like `vkDestroyCommandPool` *do* take the `VkDevice` that created the command pool. The same goes for most objects created by a device. Plus, you can't destroy a device unless all of its queues are idle *and* you have destroyed every single object created for it. This is simply not something that RAII can handle well in an application of significant complexity (ie: not a tutorial). – Nicol Bolas Feb 23 '23 at 18:43
  • @NicolBolas, still not sure what's the problem. Device + Pool is the identity for RAII object to destroy the pool. The sequence would be ... -> init L device -> init pool <-> destroy pool -> destroy L device. Device and Pool are still the opaque handlers, creating the wrapped pool will just mean, that I'd pass the device handler, but will not transfer the ownership into pool itself on creation. Are you suggesting to make a full OOP wrapper which will maintain hierarchy of such calls? – Artsiom Miksiuk Feb 23 '23 at 19:15
0

I think you forgot the & on your "instance" object when passing it to the std::unique_ptr constructor:

#include <iostream>
#include <memory>

struct Foo
{
};

int main()
{
    Foo f;
    auto deleter = [](Foo* f){std::cout << "deleting it!\n";};
    auto ptr = std::unique_ptr<Foo, decltype(deleter)>(&f, deleter);
}

You can try it here: https://tio.run/##RY1BDoIwFETX/FN8MTGtwQvQytJLqDGkFNIE2gq/C0I4e61E4vbNzBvl/aVTKsajsaoPjUZp3ESjrocK/mzQgxvnCiBFQRHenIMFVgFgLOFQG8t4Alni2ArI6kAOG91r0iNe8f5kKTljy5eJmrJULhBKiflWMbZDQ4eHzcW6bz19d1s5WPMO@pWITJIiaVVPs9fs5@cVO7XF/sYFrDF@AA

George
  • 2,034
  • 1
  • 15
  • 16
0

The problem is that VkInstance is a pointer, not "pointed-to-type", so basically you should have something like std::unique_ptr<VkInstancePointee>. But since we don't have this type, and this is basically implementation detail of Vulkan which can be changed without notice its better not to use it. So if you want RAII behavior you just need to create wrapper. Something like this:

class VkInstanceWrapper
{
public:
  static VkInstanceWrapper Create(const VkInstanceCreateInfo* createInfo) {
    return VkInstanceWrapper(createInfo);
  }

  VkInstanceWrapper(const VkInstanceCreateInfo* createInfo) {
    if (vkCreateInstance(createInfo, nullptr, &instance) != VK_SUCCESS) {
      throw std::runtime_error("failed to create vulkan instance");
    }
  }

  ~VkInstanceWrapper() {
    vkDestroyInstance(&instance, nullptr);
  }

  // To use in other vulkan calls
  operator VkInstance *() {
    return &instance;
  }

  // You need to delete copy constructor/assignment
  VkInstanceWrapper(const VkInstanceWrapper&) = delete;
  VkInstanceWrapper& operator=(const VkInstanceWrapper&) = delete;

  // You can either define or delete move constructor/assignment
  VkInstanceWrapper(VkInstanceWrapper&&) = delete;
  VkInstanceWrapper& operator=(VkInstanceWrapper&&) = delete;

private:
  VkInstance instance;
};
sklott
  • 2,634
  • 6
  • 17
  • 2
    "*since we don't have this type, and this is basically implementation detail of Vulkan which can be changed without notice its better not to use it*" The type is available; it looks like `std::remove_pointer_t`. And if that type changes, that's fine; you'd need to recompile your code, and it will change with that. – Nicol Bolas Feb 21 '23 at 04:40
  • This sort of a solution I'm trying to avoid) Really an overkill. – Artsiom Miksiuk Feb 21 '23 at 08:28
0

So, after researching a bit more, I think I need something like this:

  1. https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3677.html
  2. https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3830.pdf
  3. https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3949.pdf

Found out these from here: One-liner for RAII on non pointer?

And those are not implemented, but conforms to the general RAII concept. So, seems like unique pointers are only for pointer allocated resources, not for the value resources, which opaque handles in my case are.

Artsiom Miksiuk
  • 3,896
  • 9
  • 33
  • 49