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

Posted

技术标签:

【中文标题】我们能否增加这种面向密钥的访问保护模式的可重用性?【英文标题】:Can we increase the re-usability of this key-oriented access-protection pattern? 【发布时间】:2011-03-20 11:43:08 【问题描述】:

我们能否提高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
;

为避免继续产生误解,此模式与Attorney-Client 成语不同:

它可以比 Attorney-Client 更简洁(因为它不涉及通过第三类代理) 它可以允许访问权限的委派 ...但它对原始类也更具侵入性(每个方法一个虚拟参数)

(在this question 中开发的边讨论,因此我要打开这个问题。)

【问题讨论】:

假设的朋友会员资格暂且不谈无意义的 foo-bar 示例-您能否提供一个“实用”示例,其中该模式的使用优于其他一些更简单的技术,此外还有什么 C#还是 Java 等价物? @Beh:每当您必须限制对资源的访问,但又不想授予特权客户端完全访问权限(很少需要)以保留封装时。链接的律师 - 客户文章更详细。作为一个实际的例子,例如一个案例like this - 包装类不是供公众使用的,它应该是一个不透明的助手。使用它的免费函数具有完全访问权限,尽管它只需要访问包装器get_function_pointer() 我正在尝试将它与模板类一起使用(关键的一个朋友是模板类中的一个方法)并且由于无法分离而无法弄清楚如何管理依赖项模板声明和定义。我是否可以得出结论,我不能使用它来授予模板类方法密钥访问权限? 我决定通过创建一个将模板类包装在引用中的类来解决这个问题。然后我可以将模板类转换为非模板类​​,拆分声明和定义,并正常进行,几乎不会引入任何额外的复杂性或额外的代码。 【参考方案1】:

来自@GManNickG 的精彩回答。学到了很多。在试图让它工作时,发现了几个错别字。为了清楚起见,重复了完整的示例。我的示例从@snk_kid 发布的Check if C++0x parameter pack contains a type 借用了“contains Key in 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;

【讨论】:

【参考方案2】:

我已经阅读了很多关于不可复制性的 cmets。许多人认为它不应该是不可复制的,因为这样我们就不能将它作为参数传递给需要密钥的函数。有些人甚至对它的工作感到惊讶。好吧,它确实不应该而且显然与某些 Visual C++ 编译器有关,因为我之前也有过同样的怪异现象,但现在不再使用 Visual C++12 (Studio 2013)。

但问题是,我们可以通过“基本”不可复制性来增强安全性。 Boost 版本太多了,因为它完全阻止了复制构造函数的使用,因此对于我们需要的东西来说有点太多了。我们需要的实际上是使复制构造函数私有,但不是没有实现。当然实现是空的,但它必须存在。我最近问过在这种情况下谁在调用 copy-ctor(在这种情况下,谁在调用 ProtectedMethod 时调用了 SomeKey 的复制构造函数)。答案是显然标准确保它是调用-ctor 的方法调用者,老实说这看起来很合乎逻辑。因此,通过将 copy-ctor 设为私有,我们允许好友函数(protected Bargranted Foo)调用它,从而允许 Foo 调用 ProtectedMethod,因为它使用值参数传递,但它也会阻止任何人超出Foo 的范围。

通过这样做,即使一个开发人员尝试巧妙地使用代码,他实际上也必须让Foo 完成这项工作,另一个班级将无法获得密钥,他很有可能会以这种方式几乎 100% 地意识到自己的错误(希望如此,否则他太初学者了,无法使用这种模式,或者他应该停止开发:P)。

【讨论】:

这不是一个答案,因此不应作为一个答案发布。 好的,我该怎么办?随机浏览 *** 上的帖子,希望能得到足够的答案来让我的代表站起来,以便我发表评论?您没有阅读我为无法发表评论而道歉的第一部分;)我必须有 50 个代表才能发表评论并且不能发表评论,这很愚蠢,如果人们不能做两个中的一个事情从一开始就是回答,而不是评论=/ 如果您为此道歉,做错事并不会神奇地纠正自己。正如您所说,您可以尝试回答一些问题,直到达到 50 次。 50 次代表并不那么,所以你应该能够相对快速地实现那些;) 尽量让它成为一个答案。只是要指出,我真的没有那么多时间在 *** 上实际制作内容以增加我的代表,我认为按照许多人的要求,cmets 处理应该有一个小的变化。在这种情况下,人们可以像我一样想要引入一些新的东西,一些不是答案而是部分答案的东西,这样他们就想为 StackExchange 的发展做出贡献,但他们做不到!我希望有一天能找到一些好主意 我真的希望因为问题(我不关心这个我仍然可以通过一点时间获得 50 个代表)是随着时间的推移,问题变得更加复杂,因此答案也有一天,只有一小部分人会提出问题或回答问题,从而阻止新人真正获得任何代表。这些新来者(人类就是这样进化的)仍然可以有一个小而聪明的新鲜想法(我有时会看到),这将大大增强已知的东西【参考方案3】:

我喜欢这个成语,它有可能变得更简洁、更有表现力。

在标准 C++03 中,我认为以下方式是最容易使用和最通用的。 (不过没有太大的改进。主要是节省了重复自己的时间。)因为template parameters cannot be friends,我们必须使用宏来定义密码:

// 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:确实,我认为在 C++03 中最好的方法是接受您必须手动(好吧,使用宏更容易)为每个朋友集合制作密码,然后继续使用它。我不确定您所说的评论是什么意思,但我发现 C++03 更容易,只需将实用程序的内容放入一些 passkey.hpp 标题中,然后再看一遍。 :) 宏比手动操作要干净得多。不过我真的很喜欢 C++0x 版本;事实上,最后一个参数实际上可以读作“允许这个、这个和那个”,而那个类型只是说“这是我的钥匙,让我进去”是一个梦想。 没错,锁定的 C++0x 方法的可读性很好 :) 对于评论,我的意思是更保守的指导方针或代码审查者——如果我们可以按照我们想要的方式编写所有代码,它会是另一回事(主要解决这里的宏)。 @Georg:哦,我总是可以制定自己的指导方针。 :) allow 结构非常好,我不知道 C++0x 的模板 friend goodie(我主要还是用 C++03 编码...)而且效果非常好!

以上是关于我们能否增加这种面向密钥的访问保护模式的可重用性?的主要内容,如果未能解决你的问题,请参考以下文章

通过using声明改变个别成员的可访问性

不同应用状态结构级别的reducer的可重用性

Qt C++ 项目中 Xamarin 项目中代码的可重用性

设计模式23模式介绍

第11讲——面向对象编程

Java面向对象——方法与封装