如何为 Vulkan 对象实现三法则?

How can I implement the Rule of Three for Vulkan Objects?

提问人:ModernEraCaveman 提问时间:9/16/2023 最后编辑:ModernEraCaveman 更新时间:9/20/2023 访问量:82

问:

我想创建着色器、管道、纹理和其他与 Vulkan API 一起使用的对象的简单向量,但我正在努力了解如何使用复制构造函数、移动构造函数、复制分配和移动分配操作。

我的程序构建了一个着色器结构的向量,以传递给管道进行创建:

std::vector<vk::Shader> testShaders{
    {"vertex.vert", VK_SHADER_STAGE_VERTEX_BIT},
    {"vertex.frag", VK_SHADER_STAGE_FRAGMENT_BIT}
};
vk::GraphicsPPL<triangleList> testPPL(testShaders);

上述方法返回一个验证层错误,其中着色器模块位于 。在另一个关于 Stack Overflow 的问题中,加载向量的解决方案是实现三法则,但我现在正在努力了解如何在不触发其他错误的情况下做到这一点。我尝试了几次使用 cpp 引用页面创建复制分配运算符和复制构造函数的尝试,但我似乎无法避免使我的程序崩溃。以下是我的一些尝试:VK_NULL_HANDLE

Shader(const Shader& other) // First attempt: copy constructor
{
    std::memcpy(this, other.shaderModule, sizeof(other.shaderModule));
}// exited with code -1073741819.

Shader(const Shader& other) // Second attempt
{
    vkDestroyShaderModule(GPU::device, shaderModule, nullptr);
    //delete[] shaderModule; //With or without, does not change the outcome

    std::memcpy(shaderModule, other.shaderModule, sizeof(other.shaderModule));
} // Couldn't find VkShaderModule Object 0xcdcdcdcdcdcdcdcd and VK_NULL_HANDLE

/* Third attempt */
Shader(const Shader& other)
    : shaderStage(other.shaderStage){}

Shader& operator=(const Shader& other) // III. copy assignment
{
    if (this == &other)
        return *this;
    
    VkShaderModule new_shaderModule{};
    //sizeof(new_shaderModule) returns 8   
    //sizeof(other.shaderModule) returns 8         
    std::memcpy(new_shaderModule, other.shaderModule, sizeof(other.shaderModule));
    vkDestroyShaderModule(GPU::device, shaderModule, nullptr); // No difference with or without, neither replacing or adding "delete[] shaderModule"
    shaderModule = new_shaderModule;

    return *this;
}// VK_NULL_HANDLE error

Shader(const Shader& other) // Fourth attempt - forgot to add this one
{
    std::memcpy(this, &other, sizeof(Shader));
}

我的着色器结构如下:

struct Shader {
    VkShaderModule shaderModule;
    VkShaderStageFlagBits shaderStage;
    Shader(std::string const& filename, VkShaderStageFlagBits stage)
        : shaderStage(stage)
    {
        auto code = readFile(".\\shaders\\" + filename + ".spv"); // Function omitted from post for brevity, please don't hesitate to ask if needed
        createShaderModule(code, filename);
    }
    ~Shader() {
        vkDestroyShaderModule(GPU::device, shaderModule, nullptr);  
        // Delete[] shaderModule // With or without, did not change the outcome
    }
    void createShaderModule(const std::vector<char>& code, const std::string& filename) {
        VkShaderModuleCreateInfo createInfo
        { VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO };
        createInfo.codeSize = code.size();
        createInfo.pCode = reinterpret_cast<const uint32_t*>(code.data());

        if (vkCreateShaderModule(GPU::device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) {
            throw std::runtime_error("failed to create " + filename + "!");
        }
    }
    /* My failed attempts here */
}

为了以防万一,Vulkan 是这样定义函数的:vkCreateShaderModule

// Provided by VK_VERSION_1_0
VkResult vkCreateShaderModule(
    VkDevice                                    device,
    const VkShaderModuleCreateInfo*             pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkShaderModule*                             pShaderModule);

编辑:

根据注释中的建议对着色器进行添加:struct

Shader(const Shader& other) = delete;
Shader& operator=(const Shader& other) = delete;

Shader(Shader&& other) noexcept // move constructor
    : shaderModule(std::exchange(other.shaderModule, nullptr)), shaderStage(std::exchange(other.shaderStage, nullptr)) {}

Shader& operator=(Shader&& other) noexcept // move assignment
{
    std::swap(shaderModule, other.shaderModule);
    return *this;
}

还有一些新的错误,耶......

Error   C2440
'=': cannot convert from 'nullptr' to '_Ty'
/* path */ \MSVC\14.37.32822\include\utility

Error (active) E0304
no instance of function template "std::construct_at" matches the argument list
/* path */ MSVC\14.37.32822\include\xmemory

编辑2:

更改移动定义:

Shader(const Shader& other) = delete;
Shader& operator=(const Shader& other) = delete;

Shader(Shader&& other) noexcept // move constructor
{
    std::swap(*this, other);
    // std::exchange(*this, other); // no dice either
}

Shader& operator=(Shader&& other) noexcept // move assignment
{
    std::swap(*this, other);
    return *this;
}

新错误:

Error   C2280   'vk::Shader::Shader(const vk::Shader &)'
attempting to reference a deleted function  
/* path */\MSVC\14.37.32822\include\xutility

除了注释掉两行之外,我还尝试了解决方案,而对错误没有影响。shaderModule(nullptr)delete

C++ 内存管理 Vulkan 三法则

评论

5赞 user4581301 9/16/2023
注意:您几乎永远不想分配给 或 给 。这样做是有有效的时间的,但这不是其中之一。尝试 1 执行逐位复制,因此您最终会得到两个对象,认为它们拥有相同的资源,因为复制了资源的句柄,而不是资源。memcpythis
3赞 PaulMcKenzie 9/16/2023
也许你应该只做可移动的?这意味着,只实现移动构造函数和移动赋值运算符?复制 's 在逻辑上有意义吗?ShaderShader
2赞 user4581301 9/16/2023
这就引出了一个问题:你是要复制资源,还是需要两者来“共享”资源?如果是后者,请查看 .如果是前者,您需要确保确实想要一份副本。也许禁用副本并仅实现移动操作更符合您的要求。Shaderstd::shared_ptr
2赞 PaulMcKenzie 9/16/2023
@ModernEraCaveman禁用复制构造函数。Shader(const Shader&)= delete;
2赞 user4581301 9/16/2023
Shader(const Shader& other) = delete;Shader& operator=(const Shader& other) = delete;

答:

1赞 alexpanter 9/20/2023 #1

我不禁认为你让自己变得比需要的更困难。“我想创建与 Vulkan API 一起使用的着色器、管道、纹理和其他对象的简单向量”——真的有必要吗?值得注意的是,每当调整容器大小时,都会在内存中移动对象。std::vectorstd::vector

在构建 Vulkan 图形管道对象的情况下,您需要的是 的向量,这些向量可以存储在单个“着色器”对象中。一旦创建了管道,你就可以自由地销毁这些着色器模块。(除非管道重新创建是一回事)vkShaderModule

在我自己的 Vulkan 库中(不幸的是,不能开源 atm!我使用不透明指针来表示着色器对象,如下所示:

// shaders.hpp
struct Shader; // opaque handle defined here
Shader* create_shader(std::string_view vert, std::string_view frag);
void destroy_shader(Shader* shader);
std::vector<vkShaderModule> get_shader_modules(Shader* shader);

// shaders.cpp
struct Shader {
    // definition here
};
Shader* create_shader(std::string_view vert, std::string_view frag) {
    auto shader = new Shader;
    // ...
    return shader;
}
void destroy_shader(Shader* shader) {
    // ...
    delete shader;
}

// main.cpp
auto shader = create_shader("vertex.vert", "vertex.frag");
if (shader == nullptr) { /* FAIL */ }
// use shader here...
destroy_shader(shader);
shader = nullptr;

面向对象编程机制的引入有一种过于复杂的趋势,就像这里你正在努力处理移动构造函数一样,主要是因为内存分配/释放通常放在构造函数/析构函数中。在某些情况下,这是一个很好的模式,但它可能会导致许多未解决的问题。memcpy

值得注意的是,如果你现在想移动着色器手柄,将其传递给其他函数等,你只是在移动一个指针。因此,您可以消除记住通过 const ref 传递以避免复制或验证移动/复制构造函数/赋值操作是否确实被正确调用的额外复杂性。

现在这可能是主观的,但似乎有点像为了解决一个简单的问题而发明一个非常复杂的问题。(除了 Vulkan 中没有任何东西是“容易”的)。我希望这会有所帮助。

评论

1赞 ModernEraCaveman 9/21/2023
非常感谢@alexpanter。我花了很长时间才意识到简单的指针是最好的解决方案,而且没有必要。我最终通过使用数组/指针在着色器周围传递来实现与您的解决方案类似的解决方案,其中.std::vectorvk::Shader shaders[] = { /*list constructor 1*/, /* list constructor 2 */, ... }