Чистый C++ гранулированный эквивалент друга? (Ответ: идиома адвокат-клиент)

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

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

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

6 ответов

Решение

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

Существует очень простой шаблон, который задним числом получил активное название 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-Бал, за то, что указал, как C++11 улучшил ситуацию с friend T;

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

Я полагаю, что нечто подобное уже было сделано, но беглый взгляд ничего не дал. Следующий метод (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 в один шаблонный класс, но изо всех сил пытался найти способ сделать это. Более полные, выполнимые примеры можно найти по http://cpp.sh/6ev45 и http://cpp.sh/2rtrj.

Вы можете использовать шаблон, описанный в книге Джеффа Олджера "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++, и это трудоемко, но, возможно, слишком многословно...

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

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

Нечто похожее на приведенный ниже код позволит вам детально контролировать, какие части вашего частного состояния вы публикуете через 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 ключевое слово может указывать на то, что ваш дизайн имеет недостатки, возможно, есть способ, которым вы можете сделать то, что вам нужно, без него. Я стараюсь избегать этого, но если это делает код более читабельным, поддерживаемым или уменьшает потребность в шаблонном коде, я использую его.

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

Другие вопросы по тегам