чистый C ++ гранулированный эквивалент друга?(Ответ: идиома адвокат-клиент) - PullRequest
46 голосов
/ 10 июля 2010

Почему в C ++ есть public члены, которые может вызвать каждый, и friend объявления, которые предоставляют всем private членам данные сторонним классам или методам, но не предлагают синтаксиса дляпредоставлять определенные участники определенным абонентам?

Я хочу выразить интерфейсы с некоторыми подпрограммами, которые будут вызываться только известными абонентами, без необходимости предоставлять этим абонентам полный доступ ко всем привилегированным лицам, что кажется разумным.Лучшее, что я мог придумать сам (ниже) и предложения других, пока что вращаются вокруг идиом / паттерна различной косвенности, где я действительно просто хочу получить single , простые определения классов, которые явно указывают, чтоабоненты (более детально, чем me , мои дети или абсолютно любой ) могут получить доступ к тем членам.Каков наилучший способ выразить концепцию ниже?

// Can I grant Y::usesX(...) selective X::restricted(...) access more cleanly?
void Y::usesX(int n, X *x, int m) {
  X::AttorneyY::restricted(*x, n);
}

struct X {
  class AttorneyY;          // Proxies restricted state to part or all of Y.
private:
  void restricted(int);     // Something preferably selectively available.
  friend class AttorneyY;   // Give trusted member class private access.
  int personal_;            // Truly private state ...
};

// Single abstract permission.  Can add more friends or forwards.
class X::AttorneyY {
  friend void Y::usesX(int, X *, int);
  inline static void restricted(X &x, int n) { x.restricted(n); }
};

Я далеко не гуру организации программного обеспечения, но такое чувство, что простота интерфейса и принцип наименьших привилегий прямо расходятся в этом аспекте.языка.Более понятным примером для моего желания может быть класс Person с объявленными методами, такими как takePill(Medicine *) tellTheTruth() и forfeitDollars(unsigned int), которые должны обрабатывать только Physician, Judge или TaxMan методы экземпляров / членов, соответственнорассмотреть возможность вызова.Мне не хватает одноразовых классов прокси или интерфейса для каждого основного аспекта интерфейса, но, пожалуйста, говорите, если знаете, что я что-то упустил.

Ответ принят от Дрю Холл : Доктор Доббс - дружба и идиома «клиент-адвокат»

Приведенный выше код изначально называл класс-оболочку «Proxy» вместо «Attorney» и использовал указатели вместо ссылок, нов остальном было эквивалентно тому, что нашел Дрю, которое я тогда считал лучшим общеизвестным решением.(Не слишком сильно поглаживать себя по спине ...) Я также изменил сигнатуру «limited», чтобы продемонстрировать пересылку параметров.Общая стоимость этой идиомы составляет один класс и одно объявление друга на каждый набор разрешений, одно объявление друга на один утвержденный вызывающий набор и одну оболочку переадресации для каждого открытого метода на каждый набор разрешений.Большая часть лучшего обсуждения ниже вращается вокруг образца переадресации вызовов, которого избегает очень похожая идиома «Ключ» за счет меньшей прямой защиты.

Ответы [ 6 ]

62 голосов
/ 10 июля 2010

Существует очень простой шаблон, который ретро-активно дублирован PassKey , и который очень прост в C ++ 11 :

template <typename T>
class Key { friend T; Key() {} Key(Key const&) {} };

И с этим:

class Foo;

class Bar { public: void special(int a, Key<Foo>); };

И сайт вызова, в любом методе Foo, выглядит так:

Bar().special(1, {});

Примечание: если вы застряли в C ++ 03, перейдите к концу сообщения.

Код обманчиво прост, он содержит несколько ключевых моментов, которые стоит разработать.

Суть шаблона в том, что:

  • вызов Bar::special требует копирования Key<Foo> в контексте вызывающего
  • только Foo может создать или скопировать Key<Foo>

Примечательно, что:

  • классы, полученные из Foo, не могут создавать или копировать Key<Foo>, потому что дружба не транзитивна
  • Foo сам по себе не может передать Key<Foo>, чтобы кто-либо мог позвонить Bar::special, потому что для его вызова требуется не просто удержать экземпляр, но сделать копию

Поскольку C ++ - это C ++, есть несколько ошибок, которые следует избегать:

  • конструктор копирования должен быть определен пользователем, в противном случае он равен public по умолчанию
  • конструктор по умолчанию должен быть определен пользователем, в противном случае это public по умолчанию
  • конструктор по умолчанию должен быть определен вручную , поскольку = default позволит агрегатной инициализации обойти ручной определяемый пользователем конструктор по умолчанию (и, таким образом, любой тип получит экземпляр)

Это достаточно тонко, и на этот раз я советую вам скопировать / вставить приведенное выше определение Key дословно, а не пытаться воспроизвести его из памяти.


Вариант, разрешающий делегирование:

class Bar { public: void special(int a, Key<Foo> const&); };

В этом варианте любой, имеющий экземпляр Key<Foo>, может вызвать Bar::special, поэтому, хотя только Foo может создать Key<Foo>, он может затем передать учетные данные доверенным лейтенантам.

В этом варианте, чтобы избежать утечки ключа изгоем лейтенанта, можно полностью удалить конструктор копирования, что позволяет связать время жизни ключа с определенной лексической областью действия.


А в С ++ 03?

Ну, идея похожа, за исключением того, что friend T; не вещь, поэтому нужно создать новый тип ключа для каждого держателя:

class KeyFoo { friend class Foo; KeyFoo () {} KeyFoo (KeyFoo const&) {} };

class Bar { public: void special(int a, KeyFoo); };

Шаблон достаточно повторяющийся, чтобы избежать опечаток, возможно, стоит использовать макрос.

Совокупная инициализация не является проблемой, но опять же синтаксис = default также недоступен.


Отдельное спасибо людям, которые помогали улучшить этот ответ на протяжении многих лет:

  • Luc Touraille , за указание на меня в комментариях, что class KeyFoo: boost::noncopyable { friend class Foo; KeyFoo() {} }; полностью отключает конструктор копирования и, таким образом, работает только в варианте делегирования (предотвращая сохранение экземпляра).
  • K-ballo , за указание, как C ++ 11 улучшил ситуацию с friend T;
17 голосов
/ 10 июля 2010

Идиома Адвокат-клиент может быть тем, что вы ищете. Механика не слишком отличается от вашего решения класса прокси-членов, но этот способ более идиоматичен.

2 голосов
/ 21 ноября 2015

Я знаю, что это старый вопрос, но проблема по-прежнему актуальна. В то время как мне нравится идея идиома «адвокат-клиент», я хотел прозрачный интерфейс для клиентских классов, которым был предоставлен частный (или защищенный) доступ.

Я думаю, что нечто подобное уже было сделано, но беглый взгляд вокруг ничего не дал. Следующий метод (C ++ 11 и выше) работает для каждого класса (не для объекта) и использует базовый класс CRTP, который используется «закрытым классом» для представления открытого функтора. Только те классы, которым конкретно предоставлен доступ, могут вызывать оператор функтора (), который затем напрямую вызывает связанный закрытый метод через сохраненную ссылку.

Нет никаких накладных расходов на вызовы функций, и единственные накладные расходы памяти - это одна ссылка на частный метод, который требует раскрытия. Система очень универсальна; любая сигнатура функции и возвращаемый тип допускаются так же, как и вызов виртуальных функций в закрытом классе.

Для меня главное преимущество - это синтаксис. Хотя в приватном классе требуется довольно уродливое объявление объектов-функторов, оно полностью прозрачно для клиентских классов. Вот пример, взятый из исходного вопроса:

struct Doctor; struct Judge; struct TaxMan; struct TheState;
struct Medicine {} meds;

class Person : private GranularPrivacy<Person>
{
private:
    int32_t money_;
    void _takePill (Medicine *meds) {std::cout << "yum..."<<std::endl;}
    std::string _tellTruth () {return "will do";}
    int32_t _payDollars (uint32_t amount) {money_ -= amount; return money_;}

public:
    Person () : takePill (*this), tellTruth (*this), payDollars(*this) {}

    Signature <void, Medicine *>
        ::Function <&Person::_takePill>
            ::Allow <Doctor, TheState> takePill;

    Signature <std::string>
        ::Function <&Person::_tellTruth>
            ::Allow <Judge, TheState> tellTruth;

    Signature <int32_t, uint32_t>
        ::Function <&Person::_payDollars>
            ::Allow <TaxMan, TheState> payDollars;

};


struct Doctor
{
    Doctor (Person &patient)
    {
        patient.takePill(&meds);
//        std::cout << patient.tellTruth();     //Not allowed
    }
};

struct Judge
{
    Judge (Person &defendant)
    {
//        defendant.payDollars (20);            //Not allowed
        std::cout << defendant.tellTruth() <<std::endl;
    }
};

struct TheState
{
    TheState (Person &citizen)                  //Can access everything!
    {
        citizen.takePill(&meds);
        std::cout << citizen.tellTruth()<<std::endl;
        citizen.payDollars(50000);
    };
};

Базовый класс GranularPrivacy работает путем определения 3 вложенных шаблонных классов. Первый из них, «Подпись», принимает тип возвращаемого значения функции и сигнатуру функции в качестве параметров шаблона и перенаправляет их как в метод operator () функтора, так и во второй класс шаблона гнезда, «Функция». Это параметризуется указателем на приватную функцию-член класса Host, которая должна иметь подпись, предоставляемую классом Signature. На практике используются два отдельных класса «Function»; один приведенный здесь, а другой для константных функций, для краткости опущен.

Наконец, класс Allow рекурсивно наследует от явно инстанцированного базового класса, используя механизм шаблонов с переменными числами, в зависимости от количества классов, указанных в его списке аргументов шаблона. У каждого уровня наследования Allow есть один друг из списка шаблонов, а операторы using выводят конструктор базового класса и operator () вверх по иерархии наследования в наиболее производную область.

template <class Host> class GranularPrivacy        
{
    friend Host;
    template <typename ReturnType, typename ...Args> class Signature
    {
        friend Host;
        typedef ReturnType (Host::*FunctionPtr) (Args... args);
        template <FunctionPtr function> class Function
        {
            friend Host;
            template <class ...Friends> class Allow
            {
                Host &host_;
            protected:
                Allow (Host &host) : host_ (host) {}
                ReturnType operator () (Args... args) {return (host_.*function)(args...);}
            };
            template <class Friend, class ...Friends>
            class Allow <Friend, Friends...> : public Allow <Friends...>
            {
                friend Friend;
                friend Host;
            protected:
                using Allow <Friends...>::Allow;
                using Allow <Friends...>::operator ();
            };
        };
    };
};

Я надеюсь, что кто-то найдет это полезным, любые комментарии или предложения будут приветствоваться. Это определенно все еще в стадии разработки - я бы особенно хотел объединить классы Signature и Function в один шаблонный класс, но изо всех сил пытался найти способ сделать это. Более полные, выполнимые примеры можно найти по cpp.sh / 6ev45 и cpp.sh / 2rtrj .

2 голосов
/ 10 июля 2010

Вы можете использовать шаблон, описанный в книге Джеффа Олджера 'C ++ для настоящих программистов'.У него нет особого названия, но там его называют «драгоценными камнями и гранями».Основная идея заключается в следующем: среди вашего основного класса, который содержит всю логику, вы определяете несколько интерфейсов (не реальных интерфейсов, как они), которые реализуют части этой логики.Каждый из этих интерфейсов (аспект с точки зрения книги) обеспечивает доступ к некоторой логике основного класса (драгоценный камень).Кроме того, каждый аспект содержит указатель на экземпляр драгоценного камня.

Что это значит для вас?

  1. Вы можете использовать любой фасет везде вместо драгоценного камня.
  2. Пользователям фасетов не нужно знать о структуре драгоценного камня, так как он может быть заранее объявлен и использован через PIMPL-pattern.
  3. Другие классы могут ссылаться на фасет, а не на драгоценный камень - это ответ на ваш вопрос о том, как предоставить ограниченный набор методов для указанного класса.

Надеюсь, это поможет.Если хотите, я могу опубликовать примеры кода здесь, чтобы проиллюстрировать этот шаблон более четко.

РЕДАКТИРОВАТЬ: Вот код:

class Foo1; // This is all the client knows about Foo1
class PFoo1 { 
private: 
 Foo1* foo; 
public: 
 PFoo1(); 
 PFoo1(const PFoo1& pf); 
 ~PFoo(); 
 PFoo1& operator=(const PFoo1& pf); 

 void DoSomething(); 
 void DoSomethingElse(); 
}; 
class Foo1 { 
friend class PFoo1; 
protected: 
 Foo1(); 
public: 
 void DoSomething(); 
 void DoSomethingElse(); 
}; 

PFoo1::PFoo1() : foo(new Foo1) 
{} 

PFoo1::PFoo(const PFoo1& pf) : foo(new Foo1(*(pf
{} 

PFoo1::~PFoo() 
{ 
 delete foo; 
} 

PFoo1& PFoo1::operator=(const PFoo1& pf) 
{ 
 if (this != &pf) { 
  delete foo; 
  foo = new Foo1(*(pf.foo)); 
 } 
 return *this; 
} 

void PFoo1::DoSomething() 
{ 
 foo->DoSomething(); 
} 

void PFoo1::DoSomethingElse() 
{ 
 foo->DoSomethingElse(); 
} 

Foo1::Foo1() 
{ 
} 

void Foo1::DoSomething() 
{ 
 cout << “Foo::DoSomething()” << endl; 
} 

void Foo1::DoSomethingElse() 
{ 
 cout << “Foo::DoSomethingElse()” << endl; 
} 

РЕДАКТИРОВАТЬ2: Ваш класс Foo1 может быть более сложным, например, он содержит два других метода:

void Foo1::DoAnotherThing() 
{ 
 cout << “Foo::DoAnotherThing()” << endl; 
} 

void Foo1::AndYetAnother() 
{ 
 cout << “Foo::AndYetAnother()” << endl; 
} 

И они доступны через class PFoo2

class PFoo2 { 
    private: 
     Foo1* foo; 
    public: 
     PFoo2(); 
     PFoo2(const PFoo1& pf); 
     ~PFoo(); 
     PFoo2& operator=(const PFoo2& pf); 

     void DoAnotherThing(); 
     void AndYetAnother(); 
    };
void PFoo1::DoAnotherThing() 
    { 
     foo->DoAnotherThing(); 
    } 

    void PFoo1::AndYetAnother() 
    { 
     foo->AndYetAnother(); 
    } 

Эти методы не являютсяв классе PFoo1, поэтому вы не можете получить к ним доступ через него.Таким образом, вы можете разделить поведение Foo1 на две (или более) грани PFoo1 и PFoo2.Эти классы фасетов могут использоваться в разных местах, и их вызывающая сторона не должна знать о реализации Foo1.Может быть, это не то, что вы действительно хотите, но то, что вы хотите, невозможно для C ++, и это трудоемко, но, возможно, слишком многословно ...

0 голосов
/ 12 мая 2017

Я написал небольшое улучшение к решению, предложенному Матье М. Ограничение его решения состоит в том, что вы можете предоставить доступ только одному классу. Что если я хочу разрешить доступ любому из трех классов?

#include <type_traits>
#include <utility>

struct force_non_aggregate {};

template<typename... Ts>
struct restrict_access_to : private force_non_aggregate {
    template<typename T, typename = typename std::enable_if<(... or std::is_same<std::decay_t<T>, std::decay_t<Ts>>{})>::type>
    constexpr restrict_access_to(restrict_access_to<T>) noexcept {}
    restrict_access_to() = delete;
    restrict_access_to(restrict_access_to const &) = delete;
    restrict_access_to(restrict_access_to &&) = delete;
};

template<typename T>
struct access_requester;

template<typename T>
struct restrict_access_to<T> : private force_non_aggregate {
private:
    friend T;
    friend access_requester<T>;

    restrict_access_to() = default;
    restrict_access_to(restrict_access_to const &) = default;
    restrict_access_to(restrict_access_to &&) = default;
};

// This intermediate class gives us nice names for both sides of the access
template<typename T>
struct access_requester {
    static constexpr auto request_access_as = restrict_access_to<T>{};
};


template<typename T>
constexpr auto const & request_access_as = access_requester<T>::request_access_as;

struct S;
struct T;

auto f(restrict_access_to<S, T>) {}
auto g(restrict_access_to<S> x) {
    static_cast<void>(x);
    // f(x); // Does not compile
}

struct S {
    S() {
        g(request_access_as<S>);
        g({});
        f(request_access_as<S>);
        // f(request_access_as<T>); // Does not compile
        // f({request_access_as<T>});   // Does not compile
    }
};

struct T {
    T() {
        f({request_access_as<T>});
        // g({request_access_as<T>}); // Does not compile
        // g({}); // Does not compile
    }
};

При этом используется несколько иной подход к тому, чтобы сделать объект не агрегатом. Вместо того, чтобы иметь предоставленный пользователем конструктор, у нас есть пустой частный базовый класс. На практике это, вероятно, не имеет значения, но это означает, что эта реализация является классом POD, потому что она остается тривиальной. Однако эффект должен остаться прежним, потому что никто не будет хранить эти объекты в любом случае.

0 голосов
/ 10 июля 2010

Нечто похожее на приведенный ниже код позволит вам детально контролировать, какие части вашего частного состояния вы публикуете с помощью ключевого слова friend.

class X {
  class SomewhatPrivate {
    friend class YProxy1;

    void restricted();
  };

public:
  ...

  SomewhatPrivate &get_somewhat_private_parts() {
    return priv_;
  }

private:
  int n_;
  SomewhatPrivate priv_;
};

НО:

  1. Не думаю, что оно того стоит.
  2. Необходимость использования ключевого слова friend может указывать на недостатки вашего дизайна, возможно, есть способ, которым вы можете сделать то, что вам нужно, без него. Я стараюсь избегать этого, но если это делает код более читабельным, обслуживаемым или уменьшает потребность в шаблонном коде, я его использую.

РЕДАКТИРОВАТЬ: Для меня код выше (обычно) мерзость, которая должна (обычно) не .

...