3

I know how to create a single interface that can be applied to different objects. That's not what I'm looking for. I want the opposite behavior: I want to find a mechanism to provide different interfaces to the same object, without paying for virtual calls.

Essentially, lets say I have a queue, with the methods PushMessage, PopMessage and IsEmpty:

class Queue
{
  public:
    void PushMessage(Message message);
    Message PopMessage();
    bool IsEmpty() const;
}

I want to hand someone a handle to this queue, but only let them call PopMessage and IsEmpty, and hand someone else a handle to the same queue that only lets them call PushMessage. So the owner of the queue decides who can write to it and who can remove messages from it.

One idea I had was to create two classes that hold no data members of their own, but inherit from Queue and provide proxy methods:

struct EnQueueer
  : private Queue
{
  void PushMessage(Message message){Queue::PushMessage(message);}
}

struct DeQueueer
  : private Queue
{
  Message PopMessage(){ return Queue::PopMessage();}
  bool IsEmpty() const{ return Queue::IsEmpty();}
}

But I'm not sure this is safe. The owner of the original Queue would have to upcast to EnQueueer or DeQueueer, which I'm not sure is entirely correct. Moreover, the compiler didn't like it:

static_cast<DeQueueer*>(&m_queue); // Results in compile error:
                                   // 'Queue' is an inaccessible base of 'DeQueueer'

reinterpret_cast solves the problem, but I'm not sure if it's safe. Is it safe only as long as the derived classes have no members of their own?

Kian
  • 1,654
  • 1
  • 14
  • 22
  • Is the user (this mysterious someone you're passing the handle to) allowed to create `Queue`s? – Nard Dec 20 '14 at 18:57
  • I think it would really help if you could provide the exact use case that you want, instead of having people give you an answer and you pointing out that a tiny part of it isn't what you wanted. – Nard Dec 20 '14 at 19:00
  • @Nard They have the header, there's nothing I can do to stop them. But why would they? If you mean, are they allowed to turn the DeQueuer into a Queue, they shouldn't but I don't care if they do it. I'm trying to keep them from easily accessing the methods I don't want them to access, not make it impossible. If impossible comes for free, better. – Kian Dec 20 '14 at 19:15
  • You can actually stop them if you want to. [Opaque Pointers](http://stackoverflow.com/questions/7553750/what-is-an-opaque-pointer-in-c) are one example. – Nard Dec 20 '14 at 19:17
  • @Nard Interesting, but not free. It adds indirection. I don't mind them having a Queue of their own. Or even having them go out of their way to turn the DeQueuer back into a Queue. What I'm trying to do is to be able to have multiple interfaces to an object, so that if someone asks me for a subset of the operations the object can perform, I provide an interface that allows exactly those operations and no others. The Queue is an example of the general case. – Kian Dec 20 '14 at 19:21

1 Answers1

4

You can always take advantage of implicit conversions and have an object that wraps Queue instead.

class EnQueueer {
    public:
        // implicitly convert to Queue
        EnQueueer(const Queue& queue) : m_queue(queue) { }
        operator Queue() cont { return m_queue; }
        void PushMessage(Message m) { m_queue.PushMessage(m); }
    private:
        Queue& m_queue;
};

To make it take advantage of different implementations, use template magic:

template<typename Q>
class EnQueueer {
    public:
        // implicitly convert to Queue
        EnQueueer(const Q& queue) : m_queue(queue) { }
        operator Q() cont { return m_queue; }
        void PushMessage(Message m) { m_queue.PushMessage(m); }
    private:
        Q& m_queue;
};

You can take advantage of C++ concepts in templates if you want... but not too necessary IMO.

Note that EnQueueer contains a reference to your queue, this it is important for the lifetime of the queue to encompass the lifetime of EnQueueer. You can't store EnQueueer and let Queue die. Safe uses include substituting function calls and declarations that would work fine with Queue& by EnQueueer, etc.

EyasSH
  • 3,679
  • 22
  • 36
  • I think this would be dangerous if the `Queue` it is referencing dies. Perhaps a pointer might be better? – Nard Dec 20 '14 at 19:19
  • Absolutely. But if any reference to ```m_queue``` dies then whatever @Kian is trying to do would also fail. In both cases, the it is the user's responsibility to make sure the lifetime of the original queue object surpasses that of its references/casts. In typical uses, where ```EnQueueer``` is used as a method parameter instead of ```Queue```, and is not stored as a member variable somewhere beyond the scope of ```Queue```, this will be fine. Its an important caution though, which I shall emphasize. – EyasSH Dec 20 '14 at 19:22
  • I was meaning to eventually make the EnQueueer a smart handle of some kind to avoid lifetime issues. Either implemented on top of shared_ptr or reimplement something more lightweight for my specific needs. I just wanted to figure out how to get it to work in the first place. – Kian Dec 20 '14 at 19:33
  • If you do that, then even easier, instead of ```Q&``` just have a ```shared_ptr```member variable for m_queue. If you're already putting it in a smart pointer, you are getting the restriction you wanted almost for free--- only incrementing and decrementing the refcount whenever EnQueueer goes in and out of scope. – EyasSH Dec 20 '14 at 19:35