views:

139

answers:

1

Can we increase the re-usability for this key-oriented access-protection pattern:

class SomeKey { 
    friend class Foo;
    // more friends... ?
    SomeKey() {} 
    // possibly non-copyable too
};

class Bar {
public:
    void protectedMethod(SomeKey); // only friends of SomeKey have access
};

To avoid continued misunderstandings, this pattern is different from the Attorney-Client idiom:

  • It can be more concise than Attorney-Client (as it doesn't involve proxying through a third class)
  • It can allow delegation of access rights
  • ... but its also more intrusive on the original class (one dummy parameter per method)

(A side-discussion developed in this question, thus i'm opening this question.)

+3  A: 

I like this idiom, and it has the potential to become much cleaner and expressive.

In standard C++03, I feel the following way is the easiest to use and most generic. (Not too much of an improvement, though. Mostly saves on repeating yourself.) Because template parameters cannot be friends, we have to use a macro to define passkey's:

// define passkey groups
#define EXPAND(pX) pX

#define PASSKEY_1(pKeyname, pFriend1) \
        class EXPAND(pKeyname) \
        { \
        private: \
            friend EXPAND(pFriend1); \
            EXPAND(pKeyname)() {} \
            \
            EXPAND(pKeyname)(const EXPAND(pKeyname)&); \
            EXPAND(pKeyname)& operator=(const EXPAND(pKeyname)&); \
        }

#define PASSKEY_2(pKeyname, pFriend1, pFriend2) \
        class EXPAND(pKeyname) \
        { \
        private: \
            friend EXPAND(pFriend1); \
            friend EXPAND(pFriend2); \
            EXPAND(pKeyname)() {} \
            \
            EXPAND(pKeyname)(const EXPAND(pKeyname)&); \
            EXPAND(pKeyname)& operator=(const EXPAND(pKeyname)&); \
        }
// and so on to some N

//////////////////////////////////////////////////////////
// test!
//////////////////////////////////////////////////////////
struct bar;
struct baz;
struct qux;
void quux(int, double);

struct foo
{
    PASSKEY_1(restricted1_key, struct bar);
    PASSKEY_2(restricted2_key, struct bar, struct baz);
    PASSKEY_1(restricted3_key, void quux(int, double));

    void restricted1(restricted1_key) {}
    void restricted2(restricted2_key) {}
    void restricted3(restricted3_key) {}
} f;

struct bar
{
    void run(void)
    {
        // passkey works
        f.restricted1(foo::restricted1_key());
        f.restricted2(foo::restricted2_key());
    }
};

struct baz
{
    void run(void)
    {
        // cannot create passkey
        /* f.restricted1(foo::restricted1_key()); */

        // passkey works
        f.restricted2(foo::restricted2_key());
    }
};

struct qux
{
    void run(void)
    {
        // cannot create any required passkeys
        /* f.restricted1(foo::restricted1_key()); */
        /* f.restricted2(foo::restricted2_key()); */
    }
};

void quux(int, double)
{
    // passkey words
    f.restricted3(foo::restricted3_key());
}

void corge(void)
{
    // cannot use quux's passkey
    /* f.restricted3(foo::restricted3_key()); */
}

int main(){}

This method has two drawbacks: 1) the caller has to know the specific passkey it needs to create. While a simple naming scheme (function_key) basically eliminates it, it could still be one abstraction cleaner (and easier). 2) While it's not very difficult to use the macro can be seen as a bit ugly, requiring a block of passkey-definitions. However, improvements to these drawbacks cannot be made in C++03.


In C++0x, the idiom can reach its simplest and most expressive form. This is due to both variadic templates and allowing template parameters to be friends. (Note that MSVC allows template friend specifiers as an extension; therefore one can simulate this solution):

// each class has its own unique key only it can create
// (it will try to get friendship by "showing" its passkey)
template <typename T>
class passkey
{
private:
    friend T; // C++0x, MSVC allows as extension
    passkey() {}

    // noncopyable
    passkey(const passkey&) = delete;
    passkey& operator=(const passkey&) = delete;
};

// functions still require a macro. this
// is because a friend function requires
// the entire declaration, which is not
// just a type, but a name as well. we do 
// this by creating a tag and specializing 
// the passkey for it, friending the function
#define EXPAND(pX) pX

#define PASSKEY_FUNCTION(pTag, pFunc) \
        struct EXPAND(pTag); \
        \
        template <> \
        class passkey<EXPAND(pTag)> \
        { \
        private: \
            friend pFunc; \
            passkey() {} \
            \
            passkey(const passkey&) = delete; \
            passkey& operator=(const passkey&) = delete; \
        }

// meta function determines if a type 
// is contained in a parameter pack
template<typename T, typename... List>
struct is_contained : std::false_type {};

template<typename T, typename... List>
struct is_contained<T, T, List...> : std::true_type {};

template<typename T, typename Head, typename... List>
struct is_contained<T, Head, List...> : is_contained<M, L...> {};

// this class can only be created with allowed passkeys
template <typename... Keys>
class allow
{
public:
    // check if passkey is allowed
    template <typename Key>
    allow(const passkey<Key>&)
    {
        static_assert(is_contained<Key, Keys>::value, 
                        "Passkey is not allowed.");
    }

private:
    // noncopyable
    allow(const allow&) = delete;
    allow& operator=(const allow&) = delete;
};

//////////////////////////////////////////////////////////
// test!
//////////////////////////////////////////////////////////
struct bar;
struct baz;
struct qux;
void quux(int, double);

// make a passkey for quux
PASSKEY_FUNCTION(quux_tag, void quux(int, double));

struct foo
{    
    void restricted1(allow<bar>) {}
    void restricted2(allow<bar, baz>) {}
    void restricted3(allow<quux_tag>) {}
} f;

struct bar
{
    void run(void)
    {
        // passkey works
        f.restricted1(passkey<bar>());
        f.restricted2(passkey<bar>());
    }
};

struct baz
{
    void run(void)
    {
        // passkey does not work
        /* f.restricted1(passkey<baz>()); */

        // passkey works
        f.restricted2(passkey<baz>());
    }
};

struct qux
{
    void run(void)
    {
        // own passkey does not work,
        // cannot create any required passkeys
        /* f.restricted1(passkey<qux>()); */
        /* f.restricted2(passkey<qux>()); */
        /* f.restricted1(passkey<bar>()); */
        /* f.restricted2(passkey<baz>()); */
    }
};

void quux(int, double)
{
    // passkey words
    f.restricted3(passkey<quux_tag>());
}

void corge(void)
{
    // cannot use quux's passkey
    /* f.restricted3(passkey<quux_tag>()); */
}

int main(){}

Note with just the boilerplate code, in most cases (all non-function cases!) nothing more ever needs to be defined. This code generically and simply implements the idiom for any combination of classes and functions.

No longer does the caller need to try to create or remember a passkey specific to the function. Rather, each class now has its own unique passkey and the function simply chooses which passkey's it will allow in the template parameters of the passkey parameter (no extra definitions required); this eliminates both drawbacks. The caller just creates its own passkey and calls with that, and doesn't need to worry about anything else.

GMan
I like where you are going, but (of course a but ;)) now we are back to making a key for every type (for the moment i can't yet go for C++0x features)? Also, while your approach has other advantages, i like the simplicity of the former version. It doesn't need a supporting structure and probably has fewer problems of getting through reviews.
Georg Fritzsche
@Georg: Indeed, I think in C++03 the best way is to accept you have to manually (well, made easier with macros) make passkeys per collection of friends, and go with it. I'm not sure what you mean by reviews, but I find the C++03 much easier, just throw the utility stuff in some `passkey.hpp` header and never look at it again. :) The macro is much cleaner than doing it by hand. I really like the C++0x version though; the mere fact the last parameter can literally read "allow this, this, and that", and that types simply say "here's my key, let me in" is a dream.
GMan
True, the readability on the locked C++0x methods is nice :) With reviews i mean more conservative guide-lines or code-reviewers - if we could code everything just like we want it it would be a different matter (mainly addressing the macros here).
Georg Fritzsche
@Georg: Oh, I always get to make up my own guide-lines. :)
GMan
Then enjoy it while it lasts ;) I definitely like the improvement on readability with being able to directly pass them to `passkey` for class-types.
Georg Fritzsche
The `allow` structure is very nice, I didn't know about the template `friend` goodie of C++0x (I'm still coding in C++03 mostly...) and it does the trick very nicely!
Matthieu M.