【问题标题】: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()
  • 我正在尝试将它与模板类一起使用(关键的一个朋友是模板类中的一个方法)并且由于无法分离而无法弄清楚如何管理依赖项模板声明和定义。我是否可以得出结论,我不能使用它来授予模板类方法密钥访问权限?
  • 我决定通过创建一个将模板类包装在引用中的类来解决这个问题。然后我可以将模板类转换为非模板类​​,拆分声明和定义,并正常进行,几乎不会引入任何额外的复杂性或额外的代码。

标签: c++ design-patterns idioms friend access-protection


【解决方案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)。

    【讨论】:

    • 这不是一个答案,因此不应作为一个答案发布。
    • 好的,我该怎么办?随机浏览 StackOverflow 上的帖子,希望能得到足够的答案来让我的代表站起来,以便我发表评论?您没有阅读我为无法发表评论而道歉的第一部分;)我必须有 50 个代表才能发表评论并且不能发表评论,这很愚蠢,如果人们不能做两个中的一个事情从一开始就是回答,而不是评论=/
    • 如果您为此道歉,做错事并不会神奇地纠正自己。正如您所说,您可以尝试回答一些问题,直到达到 50 次。 50 次代表并不那么,所以你应该能够相对快速地实现那些;)
    • 尽量让它成为一个答案。只是要指出,我真的没有那么多时间在 StackOverflow 上实际制作内容以增加我的代表,我认为按照许多人的要求,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 编码...)而且效果非常好!
    猜你喜欢
    • 2011-03-20
    • 2011-03-14
    • 2019-08-04
    • 2023-03-12
    • 2020-11-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-21
    相关资源
    最近更新 更多