6

Forgive me if this has already been asked, I didn't find any answers to my specific question.

I have a class in a library I'm making that I want certain classes to be able to create and destroy, and other classes to be able to access other public functions. Having a friend class is not what I want either as the friend class will get access to member variables and member functions which I don't want. I stumbled upon this idiom which almost works, except for the destructor since it can't take additional parameters. With that idiom, I get:

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
};

As alluded to in the above code, the solution doesn't allow only class B to have access to the destructor.

While none of the above issues are deal breakers by any stretch as I can always just make ctor and dtor public and just say "RTFM".

My question is:

Is there is some way to limit access to ctor and dtor to specific classes (but only the ctor and dtor) while adhering to more well known syntax (having stuff be on the stack if people want, destroying via delete , etc.)?

Any help is greatly appreciated!

SOLUTION

in 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
};

in B.h

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

    private:
        A* m_a;
}

in 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; }

Alternatively, if the DeletableA class is needed in B.h or A.h (due to inlining, templating, or desire to have all class A related classes in A.h), it can be moved there with a "pass key" on the constructor so no other classes can create one. Even though the destructor will be exposed, no other class will ever get a DeletableA to delete.

Obviously this solution requires that class B know to make instances of Deletable A (or to make the class in general if it isn't exposed in A.h) and only store A* that are exposed via public functions, but, it is the most flexible set up that was suggested.

While still possible for some other class to make a subclass of class A (since class A isn't "final"), you can add another "pass key" to the constructor of A to prevent such behavior if you wish.

Community
  • 1
  • 1
EncodedNybble
  • 265
  • 2
  • 11
  • 3
    That's to many questions in one SO question. – 101010 Sep 18 '14 at 21:06
  • 3
    Isn't having a private destructor being a bit of a jerk? "Here's an object. You can't delete it." – tadman Sep 18 '14 at 21:07
  • Well, it's conceptually one question, but somewhat verbose. Want me to shorten it? I'll edit to shorten it. – EncodedNybble Sep 18 '14 at 21:08
  • @tadman FWIW http://stackoverflow.com/questions/631783/what-is-the-use-of-having-destructor-as-private – PaulMcKenzie Sep 18 '14 at 21:11
  • 2
    Can't really grasp everything you're asking, but my gut reaction is that you need to redesign your application. Highly complex solutions usually stem from poor design. – Frecklefoot Sep 18 '14 at 21:13
  • 2
    You know, `LifecycleKey` should have its default-ctor defined `= default;` instead of `{}` in C++11. Also, what problem are you solving by hiding your dtor? – Deduplicator Sep 18 '14 at 21:14
  • I'm not too familiar with C++11. Thanks for the tip. The basic problem is this. I have some classes in my library that, due to inlining reasons, whose headers are exposed to the users of the library. I do not want the users of the library to create any instances of the class (mainly because they don't really need to) and I also don't want them to delete any references to the class all willy nilly. So, my options are "let them do it and suffer the consequences" which is a fine solution, or "see if I can find a somewhat easy way to program around it" which is what I am trying to do here. – EncodedNybble Sep 18 '14 at 21:18

4 Answers4

1

Use a mediator-class:

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;
};

Thus, only the mediator, as part of the interface to B, gets full access.

BTW: Instead of restricting access to the dtor, you might get happier overloading new and delete and restricting access to them.
The advantage: Allocation on the stack is generally possible, if the variable is directly initialized without copying.

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;
Deduplicator
  • 44,692
  • 7
  • 66
  • 118
  • Looking at your solution, correct me if I'm wrong, but this has the same issue as potentially just having `class B` be the friend of `class A` doesn't it except that the `class mediator` now has access to the private functions and member variables of `class A`. The mediator really *shouldn't* be touching those, my question is really asking if there is a way to enforce at compilation level (without taking much time). While it works conceptually (much like just having `friend class B`) it just takes some issues and moves them from `class B` to `class mediator` does it not? – EncodedNybble Sep 19 '14 at 18:26
  • Well, you need to specify the extra-interface between `A` and `B` somewhere. This "somewhere" is the class `mediator`. See `mediator` not as a separate real class, but simply as an the interface-specification for a backdoor only `B` can use, and thus part of `A` itself. – Deduplicator Sep 19 '14 at 18:45
  • Ok, sorry, I misunderstood your solution then. If the writer of `class A` also writes the `class mediator` then there is less worry of the exposed private functions and members. While a clean solution, still doesn't support the delete keyword and new keyword (from `class B`'s) point of view and I'm unsure how one would destruct + dealloc a "static/auto/on stack" variable via the mediator interface which would be a nice feature to have. – EncodedNybble Sep 19 '14 at 19:33
  • @EncodedNybble: Simply: Not over the mediator. For that, you change `A` thus: Make the dtor public. Add overloads for the `new` `new[]` `delete` and `delete[]` operators to the private part (just calling the global ones). – Deduplicator Sep 19 '14 at 19:39
  • I see. So, with the mediator class, you can public dtor public (thus making statically allocated object able to be deleted) but override the `new` and `delete` operators for `class A` so that the dtor won't be called via keyword delete (but still can be called explicitly when using placement new?). The mediator class then handles the construction of static object, the construction of dynamic objects/pointer, and the deletion of the dynamic object? – EncodedNybble Sep 19 '14 at 19:48
1

For the goal that class B should be the only one able to instantiate and destroy objects of class A:

  • For static and automatic variable, restricting access to the constructor is all that's needed, and you're already doing that.

  • For dynamically allocated object you can restrict access to its deallocation functions, operator delete, and operator delete[], and leave the destructor public. This prohibits other code than B from deleting objects.

  • For dynamically objects you can derive class A from an interface with protected virtual destructor or named self-destroy function, which has class B as friend. B can then destroy any dynamic A object by casting up to the interface that it has access to.

Code that explicitly calls the destructor deserves whatever it gets.

Remember, you're never building an impregnable defense against malicious code, you're just building a reasonable detection and compile time reporting of inadvertent incorrect use.

Cheers and hth. - Alf
  • 142,714
  • 15
  • 209
  • 331
  • 1
    The goal here is that class `B` should be the only one able to instantiate and destroy class `A` but also shouldn't have access to `A`'s private member variables and functions. Pardon me, if I'm misunderstanding. But, seemingly, I can't restrict access to the delete and delete[] outside of using the "friend class" approach which then exposes all private members and functions (which I'd like to avoid if possible). Doesn't this approach just move the problem I'm having restricting the destructor from the destructor to these other two operators? – EncodedNybble Sep 18 '14 at 21:46
  • @EncodedNybble: sorry i wasn't thinking. extra-argument variants of `operator delete` are (paradoxically enough) only used during dynamic instantiation, for cleanup in the case placement new is used and a constructor throws. hm. okay i'm fixing the answer. – Cheers and hth. - Alf Sep 18 '14 at 22:12
  • 1
    And yes, not trying to really defend against maliciousness, just more of trying to warn against inadvertent use (i.e. "Are you sure you want to make/delete one of these? You don't really need them." and "are you sure you wanna call these private functions? You shouldn't need to" – EncodedNybble Sep 18 '14 at 22:17
  • 1
    Alright, I've tried your approach and it's oh so close to working. Perhaps I am doing something wrong. I've made the base class with protected virtual dtor, friended class `B` and had class `A` inherit from that. I also made `~A();` private; Doing this makes it so only class `B` can say `delete static_cast(pointer_to_A)`. The problem is that in making `~A()` private, means that and static/temp/auto variable made (via `public: A(LifecycleKey);` can not delete itself as the dtor is private. Making dtor public allows other classes to say `delete pointer_to_A`. Am I misunderstanding? – EncodedNybble Sep 18 '14 at 23:26
  • 1
    Now, a question may be, why is any other class, but `class B` getting a pointer to `class A` instead of references? Well, I can program around that, but I'd like to have the freedom to have `class B` have a public getter like `const std::vector getVec() const;` if it is easier for `class B` to store pointers (for whatever reason). I'd like to be flexible (to `class B` at least) if at all possible. – EncodedNybble Sep 18 '14 at 23:31
  • re "he problem is that in making ~A() private, means that and static/temp/auto variable made (via public: A(LifecycleKey); can not delete itself", presumably these are static/temp/auto objects in B code. Well I don't know any good way to support that without giving B access to just about everything. One thing you can do is to let B deal with a class derived from A, with public destructor. Since only B can instantiate A, it can enforce that every instance is of that derived class. As long as other code does not see the real type of such object, but only the A type, other code can not destroy. – Cheers and hth. - Alf Sep 19 '14 at 02:31
  • I tested it with my code and it appears to work. I've edited the original post to give a solution and I'm marking your response as an answer. – EncodedNybble Sep 19 '14 at 19:06
0

use 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();

another answer is:

#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;
}
thomas
  • 505
  • 3
  • 12
  • Using a shared_ptr with a "deleter" will work, if everything is a pointer. I'd like to find a solution that allows for temp/static/automatic variables on the stack if at all possible. It is a better solution than a `destroy` function though, thank you. – EncodedNybble Sep 19 '14 at 00:11
0

My take:

Any class/function that has access to the constructor should also have access to the destructor.

You should make ~A() public since A() is public. Since no other client except B can use the constructor, they won't have the need to use the destructor anyway.

You can further limit who can access the destructor by declaring away the copy and move constructors, and the new and delete operators.

Update

Making the destructor public and declaring away the copy and move constructors seems to address all of your concerns. You don't even need to declare away the new and delete operators or their array variants.

Here's what I think should meet most of your needs.

class B;

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

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

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

Now, let's take a look at what B can have:

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;
};

It can have the following member data types:

  1. Objects of type A.
  2. Raw pointers to objects of type A.
  3. Objects of type shared_ptr<A>.

Member functions of B can also create any of the above types of objects.

Other classes can't use objects of type A since they cannot construct one in any way. All the following attempts to use A in various forms fail.

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;
};
R Sahu
  • 204,454
  • 14
  • 159
  • 270
  • I agree they "won't have a need" to call the destructor if they can't construct one anyway. The whole purpose of restricting the ctor in the first place was to give the users "safety scissors" because I don't trust them. Thus, I don't trust them to not delete the object when they shouldn't. I have no problems with a RTFM type approach, just wondering if there is a way to dull those scissors a bit ;) – EncodedNybble Sep 18 '14 at 21:32
  • Also A() isn't "public" in the normal sense. Only classes who can create a LifecycleKey (which gets optimized out by compiler) can access A(). – EncodedNybble Sep 18 '14 at 21:40
  • Are you trying to prevent unintentional misuse or malicious abuse? – R Sahu Sep 18 '14 at 21:40
  • `A()` is public as far as `B` is concerned. – R Sahu Sep 18 '14 at 21:41
  • Preventing unintentional misuse or malicious abuse is what I'm trying to do if possible. I'm assuming a compilation error would trigger reading of comments and realization that ctor and dtor access is restricted more than "your library crashes when I delete XXXXX" which will more than likely be a support email more than reading headers and documentation sadly. – EncodedNybble Sep 18 '14 at 21:51
  • Unintentional misuse is already there by restricting who can construct instances of `A`. That won't change by moving `~A()` to the public section. Malicious abuse will be hard to prevent. A malicious user can easily edit the .h file and make their class a friend of `A`, bypassing any precautions you might have put in. – R Sahu Sep 18 '14 at 21:56
  • So, using your suggestion and declaring away (with no implementation) the new and delete operators (but not the new [] and delete []), means that all `class A` must be on the stack and be static/auto variables. Which works (the inverse of the shared_ptr with deleter idea), but I want to see if there is a way to be able to have static and dynamic objects with constructor and new/delete. – EncodedNybble Sep 19 '14 at 00:27
  • Your answer still has the issue of, if `class B` exposes a point er to an instance of `class A` via a public function, then some other class (`class C`) for example, can delete the instance of `class A` by using the delete keyword. Thus, its another thing for the implementer of `class B` to keep in mind. Not the worst thing. After seeing your edit with code examples (which are great by the way) and reading all of the other suggestions, I want to say there is no solution that has exactly what I want. This is my first SO post, so, I'm unsure what to mark as an answer in this scenario. Advice? – EncodedNybble Sep 19 '14 at 17:52