如何允许 std::unique_ptr 访问类的私有析构函数或使用私有析构函数实现 C++ 工厂类?

牙买加人

我对使用 SDL、OpenGL 和 C++ 的游戏开发非常深入,并且正在寻找方法来优化游戏在 GLSL 着色器之间为许多不同类型的不同对象切换的方式。这更像是一个 C++ 问题,而不是一个 OpenGL 问题。但是,我仍然想提供尽可能多的上下文,因为我觉得需要一些理由来说明为什么我需要的建议的 Shader 类需要按原样创建/删除。

前四个部分是我的理由、旅程和到这一点的尝试,但是我的问题很可能仅通过最后一部分就可以回答,我故意将其写成一个 tldr。

Shader 类的必要性:

当在游戏过程中创建游戏对象时,我已经看到许多 OpenGL 着色器的在线实现都是在同一个函数中创建、编译和删除的。事实证明,这在我游戏的特定部分效率低下且速度太慢。因此,我需要一个系统来在加载时创建和编译着色器,然后在游戏时间期间在它们之间间歇性地使用/交换,然后再删除。

这导致了Shader管理 OpenGL 着色器的 class( )的创建类的每个实例都应该管理一个唯一的 OpenGL 着色器,并包含一些围绕着色器类型的复杂行为,它从哪里加载,从哪里使用,它采用的统一变量等。

话虽如此,这个类最重要的作用是存储从 返回GLuint变量,并使用 this 管理与 OpenGL 着色器相关的所有 OpenGL 调用我知道鉴于 OpenGL 的全局特性,这实际上是徒劳的(因为程序中的任何地方都可以在技术上调用匹配并破坏类),但是为了有意将所有 OpenGL 调用封装到整个代码库中的非常特定区域,这系统将大大降低代码复杂性。idglCreateShader()idglDeleteShader()id

问题从哪里开始...

管理 this 最“自动”的方法GLuint id是调用glCreateShader()对象的构造和glDeleteShader()对象的销毁。这保证(在 OpenGL 限制内)OpenGL 着色器将在 C++ 着色器对象的整个生命周期中存在,并且无需调用某些void createShader()deleteShader()函数。

这一切都很好,但是在考虑复制此对象会发生什么时很快就会出现问题。如果这个对象的副本被破坏了怎么办?这意味着glDeleteShader()将被调用并有效地破坏着色器对象的所有副本。

诸如意外调用std::vector::push_back()着色器向量之类的简单错误呢?各种std::vector方法可以调用其类型的构造函数/复制构造函数/析构函数,这可能导致与上述相同的问题。

好吧...我们创建一些void createShader()deleteShader()方法怎么样即使它很乱?不幸的是,这只是推迟了上述问题,因为再次修改 OpenGL 着色器的任何调用将不同步/彻底破坏具有相同id. 在这个例子中,我限制了 OpenGL 调用glCreateShader()glDeleteShader()以保持简单,但是我应该注意到类中有许多其他 OpenGL 调用会使创建各种实例/静态变量来跟踪实例副本太复杂了证明这样做是合理的。

在进入下面的类设计之前,我想说的最后一点是,对于像原始 C++、OpenGL 和 SDL 游戏这样大的项目,我希望我犯的任何潜在的 OpenGL 错误是否会生成编译器错误与图形问题更难追查。这可以反映在下面的类设计中。

Shader该类的第一个版本

基于以上原因,我选择:

  • 制作构造函数private
  • 提供一个公共static create函数,该函数返回一个指向新 Shader 对象的指针,而不是构造函数。
  • 制作复制构造函数private
  • 制作operator= private(虽然这可能不是必需的)。
  • 将析构函数设为私有。
  • glCreateShader()在构造函数和glDeleteShader()析构函数中调用,以使 OpenGL 着色器在此对象的生命周期内存在。
  • create函数调用new关键字(并返回指向它的指针)时,Shader::create()必须delete手动调用外部调用的地方稍后会详细介绍)。

据我了解,前两个要点利用了工厂模式,如果尝试创建类的非指针类型,则会产生编译器错误。然后,第三、第四和第五个要点会阻止对象被复制。然后,第七个要点确保 OpenGL 着色器将在 C++ 着色器对象的相同生命周期内存在。

智能指针和主要问题:

我在上面唯一不喜欢的就是new/delete调用。glDeleteShader()考虑到类试图实现的封装,它们还会使对象的析构函数中的调用感觉不合适。鉴于此,我选择:

  • 改变create函数返回std::unique_ptr的的Shader类型,而不是一个Shader指针。

这个create函数看起来像这样:

std::unique_ptr<Shader> Shader::create() {
    return std::make_unique<Shader>();
}

但是随后出现了一个新问题……std::make_unique不幸的,它需要构造函数is public,这会干扰上一节中描述的必要性。幸运的是,我通过将其更改为以下内容找到了解决方案:

std::unique_ptr<Shader> Shader::create() {
    return std::unique_ptr<Shader>(new Shader());
}

但是……现在std::unique_ptr要求析构函数是公开的!这是...更好但不幸的是,这意味着可以在类外部手动调用析构函数,这反过来意味着glDeleteShader()可以从类外部调用函数。

Shader* p = Shader::create();
p->~Shader(); // Even though it would be hard to do this intentionally, I don't want to be able to do this.
delete p;

最后一节课:

为简单起见,我删除了大部分实例变量、函数/构造函数参数和其他属性,但这是最终提议的类(大部分)的样子:

class GLSLShader {

public:
    ~GLSLShader() { // OpenGL delete calls for id }; // want to make this private.

    static std::unique_ptr<GLSLShader> create() { return std::unique_ptr<GLSLShader>(new GLSLShader()); };

private:
    GLSLShader() { // OpenGL create calls for id };

    GLSLShader(const GLSLShader& glslShader);
    GLSLShader& operator=(const GLSLShader&);

    GLuint id;

};

除了析构函数是公开的这一事实之外,我对这门课中的一切都很满意。我已经对这个设计进行了测试,性能提升非常明显。尽管我无法想象我会不小心在Shader对象上手动调用析构函数,但我不喜欢它被公开暴露。我也觉得我可能会不小心漏掉一些东西,比如std::vector::push_back第二部分考虑。

我找到了解决这个问题的两种潜在解决方案。我想就这些或其他解决方案提供一些建议。

  1. 制作std::unique_ptrstd::make_uniquefriend中的Shader类。我一直在读线程,如这一个,但是这是使构造函数访问,而不是在析构函数。我也不太明白与制造所需要的缺点/额外的考虑std::unique_ptrstd::make_unique一个friend(顶端回答该线程+评论)?

  2. 根本不使用智能指针。有没有办法让我的static create()函数返回一个原始指针(使用new关键字),当Shader超出范围并调用析构函数时,它在类内部自动删除

非常感谢您抽出宝贵时间。

Yakk - Adam Nevraumont

这是一个上下文挑战。

您正在解决错误的问题。

GLuint id, 将调用glCreateShader()对象的构造和glDeleteShader()

修复这里的问题。

零的规则是,你让你的资源管理包装的寿命,你不这样做,在业务逻辑的类型。我们可以在 a 周围编写一个包装器,GLuint它知道如何清理自己并且是只移动的,通过劫持std::unique_ptr来存储整数而不是指针来防止双重破坏

来吧:

// "pointers" in unique ptrs must be comparable to nullptr.
// So, let us make an integer qualify:
template<class Int>
struct nullable{
  Int val=0;
  nullable()=default;
  nullable(Int v):val(v){}
  friend bool operator==(std::nullptr_t, nullable const& self){return !static_cast<bool>(self);}
  friend bool operator!=(std::nullptr_t, nullable const& self){return static_cast<bool>(self);}
  friend bool operator==(nullable const& self, std::nullptr_t){return !static_cast<bool>(self);}
  friend bool operator!=(nullable const& self, std::nullptr_t){return static_cast<bool>(self);}
  operator Int()const{return val;}
};

// This both statelessly stores the deleter, and
// tells the unique ptr to use a nullable<Int> instead of an Int*:
template<class Int, void(*deleter)(Int)>
struct IntDeleter{
  using pointer=nullable<Int>;
  void operator()(pointer p)const{
    deleter(p);
  }
};

// Unique ptr's core functionality is cleanup on destruction
// You can change what it uses for a pointer. 
template<class Int, void(*deleter)(Int)>
using IntResource=std::unique_ptr<Int, IntDeleter<Int,deleter>>;

// Here we statelessly remember how to destroy this particular
// kind of GLuint, and make it an RAII type with move support:
using GLShaderResource=IntResource<GLuint,glDeleteShader>;

现在该类型知道它是一个着色器并将其自身清理为非空。

GLShaderResource id(glCreateShader());
SomeGLFunction(id.get());

对于任何错别字,我们深表歉意。

类中的东西,复制 ctors 被阻止,移动 ctors 做正确的事情,dtors 自动清理等等。

struct GLSLShader {
  // public!
  ~GLSLShader() = default;
  GLSLShader() { // OpenGL create calls for id };
private: // does this really need to be private?
  GLShaderResource id;
};

简单多了。

std::vector<GLSLShader> v;

这只是有效。我们GLShaderResource是半常规的(只移动常规类型,不支持排序),并且vector对这些很满意。0 规则意味着GLSLShader拥有它的 也是半规则的并且支持 RAII——资源分配是初始化——这反过来意味着它在存储在std容器中时会正确地进行自我清理

一个类型是“Regular”意味着它“表现得像一个int”——就像原型值类型一样。当您使用常规或半常规类型时,C++ 的标准库和大部分 C++ 都喜欢它。

请注意,这基本上是零开销;与堆上没有任何内容sizeof(GLShaderResource)相同GLuint我们有一堆编译时类型的机器包装了一个简单的 32 位整数;编译时类型的机器生成代码,但不会使数据比 32 位更复杂。

活生生的例子

开销包括:

  1. 一些调用约定使传递struct包装仅与int传递不同int

  2. 在销毁时,我们检查这些中的每一个以查看是否0要决定我们是否要调用glDeleteShader编译器有时可以证明某些东西保证为零并跳过该检查。但它不会告诉你它是否确实成功了。(OTOH,众所周知,人类在证明他们跟踪所有资源方面非常糟糕,因此一些运行时检查并不是最糟糕的事情)。

  3. 如果您正在执行完全未优化的构建,则在调用 OpenGL 函数时会有一些额外的指令。但是在inline编译器进行任何非零级别的ing 之后,它们将消失。

  4. 在某些方面(可复制、可破坏、可构造),该类型不是“微不足道的”(C++ 标准中的一个术语),这使得memset在 C++ 标准下做诸如非法之类的事情你不能用一些低级的方式把它当作原始内存。


有问题!

许多 OpenGL 实现都有指向glDeleteShader/glCreateShader等的指针,上面依赖于它们是实际函数,而不是指针或宏或其他任何东西。

有两种简单的解决方法。第一个是&deleter上面参数中添加 a (两个点)。这有一个问题,它只在它们现在实际上是指针时才起作用,而在它们是实际函数时不起作用。

编写在这两种情况下都适用的代码有点棘手,但我认为几乎每个 GL 实现都使用函数指针,所以除非您想制作“库质量”实现,否则您应该很好。在这种情况下,您可以编写一些帮助程序类型来创建按名称调用(或不调用)函数指针的 constexpr 函数指针。


最后,显然有些析构函数需要额外的参数。这是一个草图。

using GLuint=std::uint32_t;

GLuint glCreateShaderImpl() { return 7; }
auto glCreateShader = glCreateShaderImpl;
void glDeleteShaderImpl(GLuint x) { std::cout << x << " deleted\n"; }
auto glDeleteShader = glDeleteShaderImpl;

std::pair<GLuint, GLuint> glCreateTextureWrapper() { return {7,1024}; }

void glDeleteTextureImpl(GLuint x, GLuint size) { std::cout << x << " deleted size [" << size << "]\n"; }
auto glDeleteTexture = glDeleteTextureImpl;

template<class Int>
struct nullable{
  Int val=0;
  nullable()=default;
  nullable(Int v):val(v){}
  nullable(std::nullptr_t){}
  friend bool operator==(std::nullptr_t, nullable const& self){return !static_cast<bool>(self);}
  friend bool operator!=(std::nullptr_t, nullable const& self){return static_cast<bool>(self);}
  friend bool operator==(nullable const& self, std::nullptr_t){return !static_cast<bool>(self);}
  friend bool operator!=(nullable const& self, std::nullptr_t){return static_cast<bool>(self);}
  operator Int()const{return val;}
};

template<class Int, auto& deleter>
struct IntDeleter;

template<class Int, class...Args, void(*&deleter)(Int, Args...)>
struct IntDeleter<Int, deleter>:
  std::tuple<std::decay_t<Args>...>
{
  using base = std::tuple<std::decay_t<Args>...>;
  using base::base;
  using pointer=nullable<Int>;
  void operator()(pointer p)const{
    std::apply([&p](std::decay_t<Args> const&...args)->void{
        deleter(p, args...);
    }, static_cast<base const&>(*this));
  }
};

template<class Int, void(*&deleter)(Int)>
using IntResource=std::unique_ptr<Int, IntDeleter<Int,deleter>>;

using GLShaderResource=IntResource<GLuint,glDeleteShader>;

using GLTextureResource=std::unique_ptr<GLuint,IntDeleter<GLuint, glDeleteTexture>>;

int main() {
    auto res = GLShaderResource(glCreateShader());
    std::cout << res.get() << "\n";
    auto tex = std::make_from_tuple<GLTextureResource>(glCreateTextureWrapper());
    std::cout << tex.get() << "\n";
}

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章

具有显式析构函数和std :: unique_ptr <>成员的类不能在std :: vector <>中使用吗?

如何通过std :: unique_ptr成员调用其他类的const成员函数

C ++ std :: unique_ptr从函数返回并测试null

为什么要为具有非平凡析构函数的类声明constrexpr构造函数(例如unique_ptr,std :: variant)

C ++如何从采用构造函数参数的类中创建std :: unique_ptr

将std :: unique_ptr类成员标记为const

当基类的成员的析构函数具有非空的no指定符和主体时,在析构函数上使用C2694

如何在类构造函数中创建新值并分配给私有unique_ptr?

如何为std :: unique_ptr创建有效的C ++别名模板

在具有受保护的析构函数的类中使用unique_ptr

std :: unique_ptr析构函数构造函数顺序

std :: unique_ptr是否在其析构函数中将其基础指针设置为nullptr?

使用已删除或非用户提供的私有析构函数构造(但不破坏)类的对象

如何正确实现C ++类的析构函数

当基类具有受保护的析构函数时,创建unique_ptr <Base>

如何将unique_ptr的移动构造函数和运算符实现为类的私有成员

如果A有析构函数,std :: unique_ptr <A>什么时候需要特殊的删除器?

错误C2248:'std :: unique_ptr <_Ty> :: unique_ptr':无法访问在类'std :: unique_ptr <_Ty>'中声明的私有成员

如何在具有受保护的析构函数和公共销毁方法的3d派对类上使用shared_ptr

C ++类变量析构函数

thread_local std :: unique_ptr版本未调用析构函数

如何释放正在返回的变量所占用的内存(使用C ++,在类中,使用析构函数)?

为什么C ++不使用std :: nested_exception允许从析构函数中抛出?

为什么在Singleton类中使用私有析构函数?

C ++继承类。将使用哪个析构函数?

C ++将删除的函数std :: unique_ptr与基类一起使用

当析构函数是私有的时,我应该如何在 c++11 中使用唯一指针?

std::unique_ptr 在虚拟析构函数上重置 SIGABRT

如何将类的静态方法作为 std::unique_ptr 的删除器传递?

TOP 榜单

热门标签

归档