我们能否增加这种基于密钥的访问保护模式的可重用性?

30

我们能否增加基于键的访问保护模式的可重用性:

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

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

为了避免持续的误解,这种模式与律师-客户成语不同:
  • 它可以比律师-客户更简洁(因为它不涉及通过第三个类进行代理)
  • 它可以允许委托访问权限
  • ...但它对原始类的干扰也更大(每个方法需要一个虚拟参数)

(在此问题中发展了一次旁边的讨论,因此我开了这个问题。)


2
撇开假想的朋友成员,也不要进入无意义的foo-bar示例——你能提供一个“实际”的例子,说明使用这种模式比其他更简单的技术更优越吗?此外,C#或Java中的等效方法是什么? - Matthieu N.
@Beh: 每当你需要限制对资源的访问,但又不想给予特权客户完全访问的权限(这是很少需要的),以保持封装性。所贴链接的律师-客户文章详细阐述了这一点。以一个实际例子来说,比如这个 - 这个包装类并非供公众使用,它应该是一个不透明的辅助类。使用它的自由函数拥有完全访问权限,尽管它只需要访问包装器的get_function_pointer()方法。 - Georg Fritzsche
我正在尝试将它与模板类一起使用(其中关键字的一个友元是模板类的方法),但是我无法弄清楚如何管理依赖关系,因为我无法分离模板声明和定义。我的结论正确吗?我不能使用这个来给模板类方法提供关键字访问吗? - iheanyi
我决定通过创建一个包装模板类引用的类来解决这个问题。然后,我可以将模板类转换为非模板类,分离声明和定义,并像正常情况下一样继续进行,而几乎不会引入任何额外的复杂性或额外的代码。 - iheanyi
3个回答

28
我喜欢这个成语,它有潜力变得更加简洁和表达力强。

在标准C++03中,我认为以下方法是最容易使用和最通用的(虽然提升不太大,但可以避免重复)。因为模板参数不能成为友元,我们必须使用宏来定义传递密钥:
// 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(){}

该方法有两个缺点:1)调用者必须知道它需要创建的特定密码。虽然一个简单的命名方案(function_key)基本上可以消除它,但它仍然可以更加抽象和简单。2)虽然使用宏不是很困难,但可能被看作是有点丑陋的,需要一块密码定义。然而,在C++03中无法改进这些缺点。
在C++0x中,这种习惯用法可以达到最简单和最具表现力的形式。这是由于变长模板和允许模板参数成为友元。(请注意,MSVC pre-2010允许模板友元说明符作为扩展;因此可以模拟这个解决方案):
// 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

// we use variadic macro parameters to allow
// functions with commas, it all gets pasted
// back together again when we friend it
#define PASSKEY_FUNCTION(pTag, pFunc, ...)               \
        struct EXPAND(pTag);                             \
                                                         \
        template <>                                      \
        class passkey<EXPAND(pTag)>                      \
        {                                                \
        private:                                         \
            friend pFunc __VA_ARGS__;                    \
            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<T, List...> {};

// 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 function
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(){}

请注意,只有样板代码,在大多数情况下(所有非函数的情况下!),不需要特别定义任何内容。该代码通用且简单地实现了任何类和函数组合的惯用语。

调用者不需要尝试创建或记住特定于函数的通行证。相反,每个类现在都有自己独特的通行证,函数仅选择它将允许的通行证的模板参数(无需额外定义);这消除了两个缺点。调用者只需创建自己的通行证并使用它进行调用,不需要担心其他任何事情。


我喜欢你的想法,但是(当然有一个“但是”;)现在我们又回到了为每种类型制作密钥的问题上(目前我还不能使用C++0x的功能)?此外,虽然你的方法有其他优点,但我喜欢以前版本的简单性。它不需要支持结构,可能会遇到更少的审核问题。 - Georg Fritzsche
@Georg:确实,我认为在C++03中,最好的方法是接受你必须手动(用宏更容易)为每个朋友集合制作通行证,并且继续前进。我不确定你所说的评论是什么意思,但我发现C++03要简单得多,只需将实用程序放入某个passkey.hpp头文件中,然后再也不看它了。:)使用宏比手工操作更清晰。我真的很喜欢C++0x版本;仅仅因为最后一个参数可以字面上读作“允许这个、这个和那个”,而类型只需说“这是我的钥匙,让我进去”就是一个梦想。 - GManNickG
没错,锁定的C++0x方法的可读性很好 :) 我所说的评论是指更为保守的指南或代码审查人员 - 如果我们能够像自己想要的那样编写所有代码,那就是另一回事了(主要是针对宏)。 - Georg Fritzsche
@Georg:哦,我总是可以自己制定指南。 :) - GManNickG
然后尽情享受它吧 ;) 我绝对喜欢可读性的提高,因为现在可以直接将它们传递给类类型的 passkey - Georg Fritzsche
3
“allow”结构非常好用,我不知道C++0x中模板“friend”的好处(我大多数还在使用C++03...),但它会非常好地完成任务! - Matthieu M.

3
我已经阅读了很多关于不可复制性的评论。许多人认为它不应该是不可复制的,因为这样我们就无法将其作为参数传递给需要密钥的函数。有些人甚至对此感到惊讶。实际上,它确实不应该是不可复制的,这似乎与某些Visual C++编译器有关,因为我以前也遇到过同样的怪异现象,但在Visual C++12(Studio 2013)中没有了。
但问题在于,我们可以通过“基本”的不可复制性来增强安全性。Boost版本太过了,因为它完全阻止了使用复制构造函数,因此对于我们所需的内容有点过头了。我们需要的是将复制构造函数设为私有,但不是没有实现。当然,实现将是空的,但必须存在。最近我问过谁在这种情况下调用了SomeKey的复制构造函数(在调用ProtectedMethod时谁调用了SomeKey的复制构造函数)。答案是,显然标准确保它是方法调用者调用-ctor,这看起来相当合理。因此,通过将copy-ctor设为私有,我们允许友元函数(protected Bargranted Foo)调用它,从而允许Foo调用ProtectedMethod,因为它使用值参数传递,但也防止了Foo范围之外的任何人。
通过这样做,即使一个开发者试图在代码中玩聪明,他实际上也必须让Foo来完成工作,另一个类将无法获得密钥,他几乎100%的时间都会意识到自己的错误(希望如此,否则他太菜了无法使用这种模式,或者应该停止开发:P)。

这不是答案,因此不应该作为答案发布。 - ThreeFx
2
那么我该怎么办呢?随机浏览StackOverflow上的帖子,希望能得到足够的答案来提高我的声望,以便我可以发表评论吗?你似乎没有看到我道歉无法发表评论的第一部分 ;) 我必须拥有50个声望才能发表评论,但这很愚蠢,如果人们从一开始就不能做两件事中的其中之一,那就是回答而不是评论 =/ - Jeremy B.
如果你做错了什么,道歉并不能神奇地纠正它。就像你说的那样,你可以尝试回答一些问题,直到达到50个声望点。50个声望点并不是很多,所以你应该能够相对快速地达成这些目标 ;) - ThreeFx
2
尽我所能地尝试回答。只是想指出,我真的没有太多时间在StackOverflow上发布内容来增加我的声望,我认为根据许多人的要求,应该对评论处理进行一些小改变。人们可以像我这样想要带来一些新东西,一些不完全是答案但部分是答案的东西,以此来为StackExchange的发展做出贡献,但他们却不能!我希望有一天能找到一些好的想法。 - Jeremy B.
2
我真的希望,因为问题(我并不担心这个问题,我仍然可以在一点时间内获得50分),随着时间的推移,问题变得更加复杂,因此答案也是如此,到某一天只有一小部分人会提问或回答问题,阻止新手实际上获得任何声望。尽管如此,这些新手(这就是人类进化的方式)可能会有一个小而杰出、新颖的想法(我有时候看到过),这将极大地增强已知的东西。 - Jeremy B.
的确。StackOverflow即将完成。在那时,就不需要再提出问题了。(但是新的问题仍然会被允许,以便我们有东西可以投反对票和关闭投票。) - Praxeolitic

1
很棒的答案,来自@GManNickG。我学到了很多。在尝试让它工作时,发现了几个错别字。完整的示例为了清晰起见而重复。我的示例从Check if C++0x parameter pack contains a type中借用了@snk_kid发布的“包含关键字的Keys…”函数。
#include<type_traits>
#include<iostream>

// identify if type is in a parameter pack or not
template < typename Tp, typename... List >
struct contains : std::false_type {};

template < typename Tp, typename Head, typename... Rest >
struct contains<Tp, Head, Rest...> :
  std::conditional< std::is_same<Tp, Head>::value,
  std::true_type,
  contains<Tp, Rest...>
  >::type{};

template < typename Tp >
struct contains<Tp> : std::false_type{};


// everything is private!
template <typename T>
class passkey {
private:
  friend T;
  passkey() {}

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


// what keys are allowed
template <typename... Keys>
class allow {
public:
  template <typename Key>
  allow(const passkey<Key>&) {
    static_assert(contains<Key, Keys...>::value, "Pass key is not allowed");
  }

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


struct for1;
struct for2;

struct foo {
  void restrict1(allow<for1>) {}
  void restrict2(allow<for1, for2>){}
} foo1;
struct for1 {
  void myFnc() {
    foo1.restrict1(passkey<for1>());
  }
};
struct for2 {
  void myFnc() {
    foo1.restrict2(passkey<for2>());
   // foo1.restrict1(passkey<for2>()); // no passkey
  }
};


void main() {
  std::cout << contains<int, int>::value << std::endl;
  std::cout << contains<int>::value << std::endl;
  std::cout << contains<int, double, bool, unsigned int>::value << std::endl;
  std::cout << contains<int, double>::value << std::endl;
}

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