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

How to allow a std::unique_ptr to access a class's private destructor or implement a C++ factory class with a private destructor?

提问人:Jaymaican 提问时间:3/4/2021 最后编辑:Jaymaican 更新时间:3/18/2021 访问量:522

问:

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

前四部分是我的理由,旅程和尝试,但是我的问题可能只能通过最后一部分来回答,我故意把它写成一个tldr。

着色器类的必要性:

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

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

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

问题从哪里开始......

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

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

一些简单的错误,比如不小心调用了着色器的矢量,该怎么办?各种方法可以调用其类型的构造函数/复制构造函数/析构函数,这可能会导致与上述相同的问题。std::vector::push_back()std::vector

好吧......即使它很乱,我们也会创建一些方法怎么样?不幸的是,这只是推迟了上述问题,因为任何修改 OpenGL 着色器的调用都将使用相同的 .为了简单起见,我限制了 OpenGL 调用,但是我应该注意,类中还有许多其他 OpenGL 调用,这些调用会使创建跟踪实例副本的各种实例/静态变量过于复杂,无法证明这样做是合理的。void createShader()deleteShader()idglCreateShader()glDeleteShader()

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

该类的第一个版本:Shader

正是出于上述原因,我选择:

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

根据我的理解,前两个要点使用工厂模式,如果尝试创建类的非指针类型,将生成编译器错误。然后,第三个、第四个和第五个项目符号要点可防止复制对象。然后,第七个要点确保 OpenGL 着色器在 C++ 着色器对象的相同生存期内存在。

智能指针和主要问题:

在上面,我唯一不是忠实粉丝的是 / 调用。它们还会使对象析构函数中的调用感觉不合适,因为该类试图实现封装。鉴于此,我选择:newdeleteglDeleteShader()

  • 更改函数以返回类型的 A 而不是指针。createstd::unique_ptrShaderShader

然后,该函数如下所示:create

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

但随后出现了一个新问题...... 不幸的是,要求构造函数是 ,这会干扰上一节中描述的必要性。幸运的是,我找到了一个解决方案,将其更改为:std::make_uniquepublic

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

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

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;

};

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

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

  1. make 或 a 的类。我一直在阅读这样的线程,但是这是为了使构造函数可访问,而不是析构函数。我也不太了解制作或(该线程的最佳答案 + 评论)所需的缺点/额外考虑因素?std::unique_ptrstd::make_uniquefriendShaderstd::unique_ptrstd::make_uniquefriend

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

非常感谢您抽出时间接受采访。

C++ 析构函数 smart-pointers friend-class

评论

0赞 super 3/4/2021
我没有阅读你的所有问题,但我认为足以理解这个想法。在我看来,您真正需要的只是一个返回 s 的工厂方法?这难道不能解决你所有的问题吗?std::shared_ptr
2赞 super 3/4/2021
此外,试图“保护用户”免于手动调用析构函数的想法至少可以说是可疑的。如果他们想打破你的班级,他们总是可以做到的。没有人会意外地手动调用析构函数。
4赞 StoryTeller - Unslander Monica 3/4/2021
防止墨菲,而不是马基雅维利。这个练习过度考虑了(几乎绝对肯定)不会成为问题的东西的设计。
0赞 davidbak 3/4/2021
“手动调用析构函数”?谁来做?如果有人恶意或无知,他们只需编辑您的头文件并删除 .或者添加好友。或者为所欲为。@StoryTeller-Unslander莫妮卡说得很好:“防止墨菲,而不是马基雅维利。无论如何,谁会针对这个接口进行编程您不会犯手动调用析构函数的错误......private:
1赞 super 3/4/2021
@Jaymaican我理解你的想法。但是,您有一个智能指针来管理对象的生存期,这一事实消除了所有这些担忧。智能指针永远不会复制基础对象(除非由用户显式完成,这在这里也是不可能的,因为你有私有的复制 ctor 和分配)。如果工厂方法是创建实例的唯一方法,那么您的基础就已经覆盖了。

答:

1赞 aleck099 3/4/2021 #1

自己实现一个删除器,让删除器成为你类的朋友。 然后像这样编辑你的声明:

static std::unique_ptr<GLSLShader, your_deleter> create();

评论

0赞 Jarod42 3/4/2021
我想说的是,会这样做的邪恶人仍然可以这样做或类似。p->~Shader()your_deleter{}(p)
0赞 Jaymaican 3/4/2021
谢谢@aleck099这是我最初寻找的解决方案类型,并且几乎解决了我在帖子中提到的问题。只是给任何未来的读者一个注释,其他人也在原始帖子的评论中提出了一些好的观点,说明为什么这首先不是一个大问题。
0赞 aleck099 3/4/2021
事实上,没有人会直接调用 dtor,所以实际上没有必要将任何 dtor 设为私有。
3赞 Yakk - Adam Nevraumont 3/4/2021 #2

这是一个上下文挑战。

你正在解决错误的问题。

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

在此处解决问题。

零法则是让资源包装器管理生存期,而不是在业务逻辑类型中这样做。我们可以围绕一个包装器编写一个包装器,它知道如何清理自己并且是仅移动的,通过劫持来存储一个整数而不是指针,从而防止双重破坏。GLuintstd::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>;

现在,该类型知道它是一个着色器,并以非 null 方式清理它。

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

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

在你的班级中填充,复制 ctor 被阻止,移动 ctor 做正确的事情,dtor 自动清理,等等。

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;

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

“常规”类型意味着它“行为类似于”——类似于原型值类型。C++ 的标准库和大部分 C++ 在使用常规或半常规类型时都喜欢它。int

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

活生生的例子

间接费用包括:

  1. 某些调用约定使传递包装与传递 .structintint

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

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

  4. 该类型在几个方面(可复制、可销毁、可构造)并不“微不足道”(C++ 标准中的一个术语),这使得在 C++ 标准下做类似的事情是非法的;你不能以一些低级的方式将其视为原始内存。memset


一个问题!

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

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

制作在这两种情况下都能正常工作的代码有点棘手,但我认为几乎每个 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";
}

评论

1赞 Jaymaican 3/7/2021
谢谢你的精彩回答。我希望我能更改我的帖子名称,以便更多寻求 GLSL 着色器/程序类整体帮助的未来用户更有可能看到这一点。我只有几个快速的后续问题。您介意简要解释关键字在 nullable 中的作用吗?还想知道关键字的作用是什么,如果它不存在会发生什么?再次感谢。friendstructconstoperator Int()const{return val;}
0赞 Jaymaican 3/7/2021
对不起,只是一个快速的补充说明。我试图实现这一点,但遇到了编译错误......。我应该注意我正在使用 GLEW v2.1。在“glew.h”中,有.它给出错误:。解决此问题的最佳方法是 glDeleteShader 的包装器吗? ?error C2975: 'deleter': invalid template argument for 'IntResource', expected compile-time constant expression#define glDeleteShader GLEW_GET_FUN(__glewDeleteShader)argument of type "PFNGLDELETESHADERPROC" is incompatible with template parameter of type "void (*)(GLuint)"void glDeleteShaderWrapper(GLuint id) { glDeleteShader(id); }
2赞 Yakk - Adam Nevraumont 3/7/2021
@Jaymaican 所以这里有一个非函数指针。但是指向该函数指针的指针将是 。所以。。。向 添加另一个间接级别。就像在测试用例中工作一样简单(在上面的代码中使用任何地方,而不仅仅是一个地方,否则你会在其他地方得到转换错误)。或者,您可以将其包装在 lambda 中。glDeleteShaderconstexprcontexprvoid(*deleter)(Int)void(*&deleter)(Int)deleter
0赞 Jaymaican 3/18/2021
嗨,再次感谢 - 我有一个后续问题,我相信未来的用户可能会遇到。OpenGL 中有一些调用,例如 glGenVertexArrays(),它接受两个参数(一个 size 和一个 id)。您可能希望跟踪大小和 ID,以便以后正确删除。继上述实现之后,您如何支持与该类型等效的而不是类型,使用包含 a 和 a 以及采用多个参数的适当删除器?GLShaderResource=IntResource<GLuint,glDeleteShader>;GLuintstructGLuintGLsizei
0赞 Yakk - Adam Nevraumont 3/18/2021
@Jaymaican创建一个资源包装器,该包装器采用元组并使用 ?或者也许不那么通用;这取决于您使用该尺寸的频率。如果大小仅在销毁时使用(或几乎仅使用),请将其推入删除程序并使其非无状态。std::tuple<some_nullable_type, Ts...>std::apply