Подтвердить что ты не робот

Чистый эквивалент эквивалентного эквивалента С++? (Ответ: Идиома Адвокат-Клиент)

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

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

// 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) что только TaxMan экземпляров/членов Physician, Judge или TaxMan, соответственно, должны даже учитывать вызов. Потребность в одноразовых прокси или классах интерфейса для каждого важного аспекта интерфейса меня не устраивает, но, пожалуйста, говорите, если знаете, что я что-то упустил.

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

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

4b9b3361

Ответ 1

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

Ответ 2

Существует очень простой шаблон, который ретро-активно дублируется PassKey и который очень легко в С++ 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, {});

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

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

Суть шаблона такова:

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

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

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

Поскольку С++ - это С++, есть несколько ошибок:

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

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


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

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, указав, как С++ 11 улучшил ситуацию с помощью friend T;

Ответ 3

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

Что это значит для тебя?

  1. Вы можете использовать любой аспект везде вместо драгоценного камня.
  2. Пользователям фасетов не нужно знать о структуре драгоценных камней, так как они могут быть заранее объявлены и использованы через PIMPL-шаблон.
  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; 
} 

EDIT2: Ваш класс 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++, и это трудоемкий, но, возможно, слишком многословный...

Ответ 4

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

Я предполагаю, что что-то похожее на это уже сделано, но беглый взгляд вокруг ничего не появлялся. Следующий метод (С++ 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"; тот, который указан здесь, а другой для функций const, опущен для краткости.

Наконец, класс 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.

Ответ 5

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

class X {
  class SomewhatPrivate {
    friend class YProxy1;

    void restricted();
  };

public:
  ...

  SomewhatPrivate &get_somewhat_private_parts() {
    return priv_;
  }

private:
  int n_;
  SomewhatPrivate priv_;
};

НО:

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

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

Ответ 6

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

#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, потому что она остается тривиальной. Однако эффект должен оставаться тем же, потому что никто не собирается хранить эти объекты в любом случае.