What if regular constructors were used for Vulkan handle types
Currently most Vulkan handle types are created through their parent handles:
var device = physicalDevice.CreateDevice(createInfo);
With the exception being VkInstance since it is the starting point to using Vulkan:
var instance = new Instance(createInfo);
The benefits of creating handle types through their parents are:
- It is guaranteed that the parent is from the same Vulkan instance or device
The downsides are:
- It is more intuitive to construct new instances of types through the
newkeyword - It can be confusing to understand from under which handle type a new handle should be created (for example, should ImageView be created through Image or Device - currently it's the former)
- There's an indirection involving an additional method call which may or may not be inlined
What if all the handle types were created through their constructors? That would convert the example above to the following:
var device = new Device(physicalDevice, createInfo);
Additionally, handle types which can be bulk-created (such as vkCreateGraphicsPipelines) could be turned into a static method on the type:
Pipeline[] pipelines = Pipeline.New(device, pipelineCache, createInfos);
Finally, there's the question if allocation operations (such as vkAllocateCommandBuffers) should also follow the previous example with method name replaced:
CommandBuffer[] cmdBuffers = CommandBuffer.Allocate(cmdPool, allocateInfo);
Or be kept unchanged:
CommandBuffer[] cmdBuffers = cmdPool.AllocateBuffers(allocateInfo);
Pros for the new pattern:
Generic dependency injection is easier to implement if the constructors are the preferred manner of instantiation. E.g. var device = VulkanProvider.Require<Device>(); could resolve the constructor for Device, decide what constructors can be filled, and then inject dependencies for the constructors for it's arguments by providing existing values where appropriate or constructing more, and following the same pattern for those arguments which require construction, until the dependency is filled.
The behavior of constructors also implies provisioning an instance of the specific constructed type or to throw an exception, limiting required surrounding validation code.
Pros for the factory pattern; It would allow you to construct a type that implements the expected type without actually being the same type (preferably the factories will be filling from the types declared as abstract classes), should you want to implement such a thing.
So long as the method name(s) followed a pattern, a specific dependency injection scheme could still be built around it easily enough.
If creating 'null' is ever the correct answer given some arguments, you are able to do so.
That's it for the pros... other thoughts;
Performance for instantiation of structs and classes should be overlooked for ease of implementation where performance is not critical (e.g. device creation, context initialization).
Special performance paths should be created for bulk initialization and interaction as needed afterward.
Thanks for the input, @TYoung86 !
Factory and constructor patterns could live side-by-side, but that might cause confusion and makes way for a lot of code/doc duplication.