限制访问C++构造函数和析构函数

6

如果这个问题已经被问到了,请原谅我,我没有找到对我的具体问题的答案。

我正在制作一个库中的类,我希望某些类能够创建和销毁,而其他类则可以访问其他公共函数。使用 friend class 也不是我想要的,因为友元类将获得访问成员变量和成员函数的权限,而我不希望它们这样做。我偶然发现了这个习语,它几乎可以实现我想要的功能,但析构函数由于不能接受额外的参数而无法使用。使用该习语,我获得:

class B;
class A
{
    public:
        class LifecycleKey
        {
            private:
                LifecycleKey() {}
                friend class B;
        };

        A(LifecycleKey); // Now only class B can call this
        // Other public functions

    private:
        ~A(); // But how can I get class B to have access to this?

        void somePrivateFunction();

        // Members and other private functions
};

正如上述代码所提到的,该解决方案不允许只有class B可以访问析构函数。
虽然以上问题都不是致命的问题,因为我可以将构造函数和析构函数设为公共,然后说“RTFM”。
我的问题是:
是否有一种方法可以限制对特定类的构造函数和析构函数的访问(但仅限于构造函数和析构函数),同时遵循更为常见的语法(如果人们想要在堆栈上放置东西,则通过delete进行销毁等)?
非常感谢任何帮助!
解决方案:
A.h中。
class B;
class A
{
    protected:
        A() {}
        virtual ~A() {}
        A(const A&); // Implement if needed
        A(A&&); // Implement if needed

    public:
        // Public functions

    private:
        void somePrivateFunction();

        // Members and other private functions
};

B.h

class B
{
    public:
        B();
        ~B();
        const A* getA() const;

    private:
        A* m_a;
}

B.cpp

namespace {
    class DeletableA : public A {
        public:
            DeletableA() : A() {}
            DeletableA(const DeletableA&); // Implement if needed
            DeletableA(DeletableA&&); // Implement if needed
            ~DeletableA() {}
    }
}

#include B.h
B::B() : m_a(new DeletableA()) {}
B::~B() { delete static_cast<DeletableA*>(m_a); }
const A* B::getA() const { return m_a; }

另外,如果需要在B.hA.h中使用 DeletableA 类(由于内联、模板化或希望所有与class A 相关的类都在A.h中),可以在构造函数上使用 "pass key" 将其移动到那里,以便没有其他类可以创建它。即使析构函数将被公开,也不会有其他类获得 DeletableA 去删除。

显然,这种解决方案要求 class B 知道如何创建 Deletable A 实例(或者如果在 A.h 中未公开,则知道如何创建该类)并仅存储通过公共函数公开的 A*,但是,这是建议中最灵活的设置。

虽然某些其他类仍然可以创建 class A 的子类(因为 class A 不是“final”),但是您可以在A的构造函数中添加另一个“pass key”来阻止这种行为,如果您愿意。


3
这个SO问题包含了太多的问题。 - 101010
3
拥有私有析构函数是不是有点刻薄?“这里有个对象,你不能删除它。” - tadman
@tadman 值得一提的是 https://dev59.com/mHRB5IYBdhLWcg3wa2m2 - PaulMcKenzie
2
我无法完全理解你所问的一切,但我的直觉反应是你需要重新设计你的应用程序。高度复杂的解决方案通常源于糟糕的设计。 - Frecklefoot
2
你知道吗,在C++11中,LifecycleKey应该定义为默认构造函数= default;而不是{}。另外,通过隐藏析构函数,你解决了什么问题? - Deduplicator
显示剩余2条评论
4个回答

1
使用中介者类:
class mediator;
class A
{
/* Only for B via mediator */
    A();
    ~A(); // But how can I get class B to have access to this?
    friend class mediator;
/* Past this line the official interface */
public:
    void somePrivateFunction();
protected:
private:
};

class B;
class mediator
{
    static A* createA() { return new A{}; }
    static void destroyA(const A* p) { delete p; }
    // Add additional creators and such here
    friend class B;
};

因此,只有作为与B接口的一部分的中介者才能获得完全访问权限。

顺便说一句:不要限制对 dtor 的访问,您可能会更喜欢重载newdelete并限制对它们的访问。
好处是:如果变量直接初始化而不是复制,则通常可以在堆栈上分配内存。

void* operator new(std::size_t);
void* operator new[](std::size_t);
void operator delete(void*);
void operator delete[](void*);
void operator delete(void*, std::size_t) noexcept;
void operator delete[](void*, std::size_t) noexcept;

看了你的解决方案,如果我说错了,请纠正我,但这与只让class B成为class A的友元有相同的问题,除了现在class mediator可以访问class A的私有函数和成员变量。中介者确实不应该触及那些内容,我的问题是是否有一种方法可以在编译级别强制执行(而不需要花费太多时间)。虽然它在概念上可行(就像只有friend class B一样),但它只是将一些问题从class B转移到了class mediator,不是吗? - EncodedNybble
你需要在某处指定AB之间的额外接口。这个“某处”就是中介者类。把中介者类看作一个独立的实际类,而只是B可以使用的后门接口规范,并且是A本身的一部分。 - Deduplicator
好的,抱歉,我误解了你的解决方案。如果A类的编写者也编写了中介者类,那么就不用太担心暴露私有函数和成员的问题了。虽然这是一个干净的解决方案,但仍然不支持从B类的角度看到的删除关键字和新关键字,并且我不确定如何通过中介者接口销毁+释放“静态/自动/栈上”的变量,这将是一个很好的功能。 - EncodedNybble
@EncodedNybble:简单来说,不要超过中介者。为此,您需要更改 A 如下:将 dtor 设为 public。在私有部分添加 newnew[]deletedelete[] 运算符的重载(只需调用全局运算符)。 - Deduplicator
我明白了。因此,使用中介类,您可以公开dtor public(从而使静态分配的对象能够被删除),但是覆盖class Anewdelete运算符,以便通过关键字delete不会调用dtor(但仍然可以在使用放置new时显式调用?)。中介类然后处理静态对象的构建、动态对象/指针的构建和动态对象的删除? - EncodedNybble

1
为了让类 B 成为唯一能够实例化和销毁类 A 对象的类:
  • 对于静态和自动变量,限制对构造函数的访问就足够了,而您已经在做了。

  • 对于动态分配的对象,您可以限制对其释放函数、operator deleteoperator delete[] 的访问,并将析构函数设置为公共。这样可以禁止除 B 之外的其他代码删除对象。

  • 对于动态对象,您可以从一个带有 protected 虚析构函数或命名的自毁函数的接口派生出类 A,并将类 B 设置为友元。然后,B 可以通过向上转型到它可以访问的接口来销毁任何动态 A 对象。

明确调用析构函数的代码应该得到相应的结果。

记住,您永远不会建立针对恶意代码的固若金汤的防御措施,您只是在建立一个合理的检测和编译时报告意外错误使用的机制。


1
这里的目标是,类B应该是唯一能够实例化和销毁类A的类,但也不应该访问A的私有成员变量和函数。如果我误解了,请原谅。但是,似乎我无法限制在使用“友元类”方法之外删除和delete[]的访问,这将暴露所有私有成员和函数(如果可能的话,我想避免这种情况)。这种方法是否只是将我限制析构函数的问题从析构函数转移到这两个其他运算符上? - EncodedNybble
@EncodedNybble: 对不起,我没有考虑清楚。operator delete 的额外参数变量(令人矛盾的是)只在动态实例化期间使用,在放置 new 用于构造函数抛出时进行清理。嗯,好吧,我正在修正答案。 - Cheers and hth. - Alf
1
是的,不是真正地试图防御恶意行为,只是试图警告无心使用(即“您确定要创建/删除其中之一吗?您真的不需要它们。”和“您确定要调用这些私有函数吗?您不应该需要”)。 - EncodedNybble
1
好的,我尝试了你的方法,它非常接近成功了。也许是我做错了什么。我已经创建了一个带有受保护虚析构函数的基类,并将类B设置为友元类,并使类A从该类继承。我还将~A();设置为私有;这样做可以使只有类B能够使用delete static_cast<ABase*>(pointer_to_A)。问题在于,将~A()设置为私有,意味着通过public: A(LifecycleKey);创建的静态/临时/自动变量无法删除自身,因为析构函数是私有的。将析构函数设置为公共的允许其他类使用delete pointer_to_A。我是否误解了? - EncodedNybble
1
现在,一个问题可能是,为什么除了class B之外的任何其他类都会得到指向class A的指针而不是引用?好吧,我可以绕过这个问题进行编程,但如果class B更容易存储指针(出于任何原因),我想要有class B具有公共getter的自由,例如const std :: vector <const A *> getVec() const;。如果可能的话,我希望至少对class B保持灵活性。 - EncodedNybble
显示剩余2条评论

0

使用 shared_ptr

class K{
public:
    int x;
private:
    ~K(){}; 
     K(){};  
private:
    friend class K_Creater;
    friend class K_Deleter; 
};

struct K_Deleter{  void operator()(K* p) { delete p; }  };

struct K_Creater{ 
    static shared_ptr<K> Create(){ 
        return shared_ptr<K>(new K,  K_Deleter() ); 
    }
};



//K* p = new K;    prohibited
shared_ptr<K> p = K_Creator::Create();

另一个答案是:

#include <iostream>

class A
{
public:

    class Key
    {
    private:
        Key(){
            std::cout << "Key()" << std::endl;
        }
        ~Key(){
            std::cout << "~Key()" << std::endl;
        }
        friend class B;
    };

    A(Key key){
        std::cout << "A(Key key)" << std::endl;
    }
    void seti(){ i_=0;}
private:
    int i_;
};

class B{
public:
    static void foo(){

        A a{A::Key()};

        A* pa = new A( A::Key() );
        delete pa;

        static A sa({});

    }
};

int main(){

    B::foo();

    //A a{A::Key()};  prohibit
    //A* pa = new A( A::Key() );   prohibit
    //delete pa;   prohibit
    //static A sa({});   prohibit

    return 0;
}

如果所有东西都是指针,使用带有“删除器”的shared_ptr将起作用。如果可能的话,我想找到一种允许在堆栈上使用临时/静态/自动变量的解决方案。虽然比“destroy”函数更好,但这是一个更好的解决方案,谢谢。 - EncodedNybble

0

我的看法:

任何可以访问构造函数的类/函数也应该可以访问析构函数。

你应该将~A()设置为公共的,因为A()是公共的。由于除了B之外的其他客户端都不能使用构造函数,他们也不需要使用析构函数。

您可以通过声明复制和移动构造函数以及newdelete运算符来进一步限制谁可以访问析构函数。

更新

将析构函数设置为公共的并声明复制和移动构造函数似乎可以解决您所有的问题。您甚至不需要声明newdelete运算符或它们的数组变体。

以下是我认为可以满足大多数需求的内容。

class B;

class PassKey
{
   private:
      PassKey() {}
      ~PassKey() {}
   friend class B;
};

class A
{
   public:
      A(PassKey) {}
      ~A() {}

   private:
      // Declare away
      A(A const&);
      A(A&&);
};

现在,让我们来看一下 B 可以有什么:

class B
{
   public:
      B() : a(PassKey()), ap(new A(PassKey())), ap2(new A(PassKey())) {}
      ~B() { delete ap; }

      A const& getA() const {return a;}

      A a;
      A* ap;
      std::shared_ptr<A> ap2;
};

它可以具有以下成员数据类型:

  1. 类型为A的对象。
  2. 指向类型为A的对象的原始指针。
  3. 类型为shared_ptr<A>的对象。

B的成员函数也可以创建上述任何类型的对象。

其他类不能使用类型为A的对象,因为它们无法以任何方式构造一个。所有尝试以各种形式使用A的尝试都将失败。

struct C
{
   C() : a(PassKey()) {} // Can't construct an instance of A 
                         // since C doesn't have access to
                         // PassKey's constructor.
   A a;
};

struct D
{
   D() : a(new A(PassKey())) {} // Can't construct an instance of A 
                                // since D doesn't have access to
                                // PassKey's constructor.
   A* a;
};

struct E
{
   E(A const& a) : ap(new A(a)) {}  // Can't construct an instance of A 
                                    // since E doesn't have access to
                                    // A's copy constructor.
   A* ap;
};

class F
{
   public:
      F(A& a) : ap(new A(std::move(a))) {} // Can't construct an instance of A 
                                           // since F doesn't have access to
                                           // A's move constructor.
      A* ap;
};

另外,A()并不是“public”意义下的公共函数。只有能够创建LifecycleKey(由编译器优化掉)的类才能访问A()。 - EncodedNybble
你是在试图防止无意的误用还是恶意滥用? - R Sahu
如果可能的话,我正在尝试防止意外误用或恶意滥用。我假设编译错误会触发阅读注释并意识到 ctor 和 dtor 访问受到更严格的限制,而不仅仅是“当我删除 XXXXX 时,你的库崩溃了”,这很可能会成为一封支持电子邮件,而不是阅读标题和文档。 - EncodedNybble
通过限制谁可以构建 A 的实例,意外的误用已经存在。将 ~A() 移动到公共部分不会改变这一点。恶意滥用将很难防止。恶意用户可以轻松编辑 .h 文件,并将其类设置为 A 的友元,绕过您可能采取的任何预防措施。 - R Sahu
你的回答仍然存在问题,如果B类通过公共函数向A类实例公开指针,那么其他类(例如C类)可以使用delete关键字删除A类实例。因此,这是B类实现者需要记住的另一件事情。虽然不是最糟糕的事情。在看到您的编辑代码示例(顺便说一下,非常好)并阅读所有其他建议后,我想说没有完全符合我的要求的解决方案。这是我第一次在SO上发布帖子,所以我不确定在这种情况下应该标记什么作为答案。有什么建议吗? - EncodedNybble
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接