2010-07-10 10 views
40

¿Por qué C++ tiene public miembros que cualquier persona puede llamar y friend declaraciones que exponen todosprivate miembros a dados clases o métodos extranjeros, pero no ofrecen ninguna sintaxis para exponer a los miembros particulares a los que llaman dados?limpio C++ amigo granular equivalente? (Respuesta: Abogado-Cliente Idiom)

Quiero expresar las interfaces con algunas rutinas para que las invoquen solo las personas que llaman sin tener que darles acceso completo a todas las partes privadas, lo cual es razonable. Lo mejor que pude encontrar (más abajo) y las sugerencias de otros hasta ahora giran en torno a modismos/patrones de variabilidad indirecta, donde realmente solo quiero una forma de tener solo, definiciones de clase simples que indican explícitamente lo que llaman (más granularly que me, mis hijos, o cualquiera)) puede acceder a qué miembros. ¿Cuál es la mejor manera de expresar el concepto a continuación?

// 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); } 
}; 

Estoy muy lejos de ser un gurú de la organización de software, pero se siente como la sencillez de interfaz y el principio del mínimo privilegio entran en conflicto directo en este aspecto de la lengua. Un ejemplo más claro de mi deseo podría ser una clase Person con métodos declarados como takePill(Medicine *)tellTheTruth() y forfeitDollars(unsigned int) que sólo Physician, Judge, o TaxMan casos/métodos miembros, respectivamente, deberían incluso considerar la invocación. Necesito una sola proxy o clases de interfaz para cada aspecto importante de la interfaz, pero dígame si sabe que me estoy perdiendo algo.

respuesta aceptada de Drew Hall: Dr Dobbs - Friendship and the Attorney-Client Idiom

El código anterior originalmente llamado la clase contenedora 'proxy' en lugar de 'abogado' y se utiliza punteros en lugar de referencias, pero por lo demás fue equivalente a lo que dibujó encontró, que luego considerada la mejor solución conocida en general. (No darme palmaditas en la espalda demasiado fuerte ...) También cambié la firma de 'restringido' para demostrar el reenvío de parámetros. El costo total de esta expresión idiomática es una declaración de clase y una de amigo por conjunto de permisos, una declaración de amigo por llamada autorizada establecida y una versión de envío por método expuesto por conjunto de permisos. La mayor parte de la mejor discusión a continuación gira en torno a la repetición de la llamada de reenvío que una expresión idiomática 'clave' muy similar evita a costa de una protección menos directa.

+0

Creo que esta es una pregunta interesante sobre el diseño del lenguaje: ¿cómo manejaría esto su lenguaje ideal? Pero esto parece demasiado ingenioso para la pequeña cantidad de veces que se requieren amigos. Para un control de acceso más granular que público/protegido/privado, por lo general un prefijo de nombre de función o comentario es suficiente. Prefiero el enfoque de Python, que es "los usuarios deberían hacer lo correcto" en lugar del inútil intento de aplicarlo. – Stephen

+0

@Stephen: El enfoque habitual de C++ es detectar tantos errores posibles en tiempo de compilación como sea posible. Creo que el enfoque de Matthieus está cerca de lo que me gustaría de la compatibilidad con el lenguaje: Permitir el acceso por clase o función en función de los grupos. –

+0

@Georg: Creo que el enfoque de C++ es permitir el acceso en la medida en que el lenguaje lo permita. Ese nivel de control de acceso sería bueno, pero no creo que la creación de objetos de clave falsos sea algo que diseñaría en un lenguaje :) Claro, la mayoría logra el objetivo (ignorando el hecho de que las claves se pueden pasar), pero - en mi opinión, es más trabajo del que debería dedicarse al tema :) Estoy bien acordar desacuerdo sobre esto con todos ustedes, y tal vez cambiaría de opinión si trabajaba en su base de código. – Stephen

Respuesta

16

El Attorney-Client idiom puede ser lo que estás buscando. La mecánica no es muy diferente de la solución de clase de proxy de miembro, pero de esta manera es más idiomática.

+0

Lo sé solo por su nombre, y lo que he visto hasta ahora parece muy prometedor. Esto puede ser lo que estoy buscando. ¡Gracias! – Jeff

+0

Bueno, esto es casi textualmente lo que reinventé, y creo que el enlace merece una lectura. La diferencia es que en la forma publicada, la clase auxiliar se llama XAttourney en lugar de X :: Proxy2, y las llamadas envueltas utilizan referencias en lugar de punteros. Voy a dividir la diferencia, haciendo que los miembros llamados X :: YAttourney utilicen llamadas estáticas en las referencias. Como esta es una respuesta tan bien investigada y canónica como la que voy a dar para 'cómo', estoy aceptando esto y dividiré 'por qué el amigo es todo o nada' en una nueva pregunta más adelante. – Jeff

0

Algo similar al código siguiente le permitirá un control detallado sobre qué partes de su estado privado hace público a través de la palabra clave friend.

class X { 
    class SomewhatPrivate { 
    friend class YProxy1; 

    void restricted(); 
    }; 

public: 
    ... 

    SomewhatPrivate &get_somewhat_private_parts() { 
    return priv_; 
    } 

private: 
    int n_; 
    SomewhatPrivate priv_; 
}; 

PERO:

  1. no creo que vale la pena el esfuerzo.
  2. La necesidad de utilizar la palabra clave friend podría sugerir que su diseño es defectuoso, quizás haya una forma de que pueda hacer lo que necesita sin él. Intento evitarlo, pero si hace que el código sea más legible, mantenible o reduzca la necesidad de un código repetitivo, lo uso.

EDIT: Para mí el código anterior es (generalmente) una abominación que debe (por lo general) no usarse.

+0

El uso de la amistad no implica que un diseño sea defectuoso. Un uso inapropiado de la amistad sí lo hace, pero del contexto de esta pregunta no podemos inferir eso. La gente teme la amistad porque, como afirman, viola la encapsulación. En cambio, cuando se usa correctamente, en realidad beneficia la encapsulación. [Ref Stroustrup] – Shirik

+0

Shirik: Por lo tanto, "podría sugerir" – Staffan

+0

La afirmación "podría sugerir" se basa completamente en el predicado "existe el uso de la amistad". Esta correlación no debería existir. – Shirik

2

Puede usar un patrón descrito en Jeff Aldger's book 'C++ para programadores reales'. No tiene un nombre especial, pero allí se lo conoce como 'piedras preciosas y facetas'. La idea básica es la siguiente: entre su clase principal que contiene toda la lógica, usted define varias interfaces (no interfaces reales, como ellas) que implementa sub-partes de esa lógica. Cada una de esas interfaces (faceta en términos de libro) proporciona acceso a algunos de la lógica de la clase principal (gema). Además, cada faceta contiene el puntero a la instancia de piedra preciosa.

¿Qué significa esto para usted?

  1. Puede usar cualquier faceta en cualquier lugar en lugar de piedras preciosas.
  2. Los usuarios de facetas no tienen que saber acerca de la estructura de la piedra preciosa, ya que podría ser declarada y utilizada a través del patrón PIMPL.
  3. Otras clases pueden referirse a la faceta más bien a la piedra preciosa: esta es la respuesta a su pregunta sobre cómo exponer una cantidad limitada de métodos a la clase especificada.

Espero que esto ayude. Si lo desea, podría publicar ejemplos de código aquí para ilustrar este patrón más claramente.

EDIT: Aquí está el código:

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: Su foo1 clase podría ser más compleja, por ejemplo, contiene dos otros métodos:

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

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

y Se puede acceder a través de 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(); 
    } 

Esos métodos no están en la clase PFoo1, por lo que no puede acceder a ellos a través de él. De esta manera, puede dividir el comportamiento de Foo1 en dos (o más) facetas PFoo1 y PFoo2. Esas clases de facetas podrían usarse en diferentes lugares, y su interlocutor no debería estar al tanto de la implementación de Foo1. Tal vez no es lo que realmente quieres, pero lo que quieres es imposible para C++, y este es un aroud de trabajo, pero tal vez demasiado detallado ...

+0

Gracias por la publicación, pero no estoy seguro de tener claro el propósito de este patrón. Parece que el ocultamiento de la implementación de vanilla pImpl-style, donde solo quiero una descripción de interfaz plana pero selectiva. ¿Podrías exponer un poco sobre sus matices? – Jeff

+0

Claro. Vea la edición2. – Haspemulator

52

Hay un patrón muy simple, que ha sido apodado retroactivamente PassKey y que es very easy in C++11:

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

y con eso:

class Foo; 

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

y el sitio de llamadas, en cualquier método Foo, se ve así:

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

Nota: si está atrapado en C++ 03, omita hasta el final de la publicación.

El código es engañosamente simple, incorpora algunos puntos clave que vale la pena elaborar.

El quid de la pauta es que:

  • llamando Bar::special requiere la copia de un Key<Foo> en el contexto de la persona que llama
  • única Foo puede construir o copiar un Key<Foo>

Es notable que:

  • clases derivadas de Foo no puede construir o copiar Key<Foo> porque la amistad no es transitivo
  • Foo sí misma no puede dictar una Key<Foo> para que cualquiera pueda llamar Bar::special porque llamándolo requiere no sólo que se aferra a una instancia, pero hacer una copia

Debido C++ es C++, hay algunas trampas para evitar:

  • el constructor de copia tiene que ser definida por el usuario, de lo contrario es public por defecto
  • constructor el defecto tiene que ser definida por el usuario, de lo contrario es public por defecto
  • el constructor por defecto tiene que ser manualmente definida, porque = default permitiría inicialización de agregados para eludir el manual constructor por defecto definido por el usuario (y por lo tanto permite ningún tipo para obtener una instancia)

Esto es lo suficientemente sutil que, por una vez, yo le aconsejo que copiar/pegar la definición anterior de Key pie de la letra en lugar de tratar de reproducir de memoria.


Una variación permitir la delegación:

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

En esta variante, cualquier persona que tenga una instancia de Key<Foo> puede llamar Bar::special, por lo que incluso aunque sólo Foo puede crear un Key<Foo>, entonces se puede difundir las credenciales para tenientes de confianza

En esta variante, para evitar que un teniente pícaro gotee la clave, es posible eliminar el constructor de copia por completo, lo que permite vincular el tiempo de vida de la clave a un alcance léxico particular.


Y en C++ 03?

Bueno, la idea es similar, excepto que friend T; no es una cosa, así que hay que crear un nuevo tipo de clave para cada titular:

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

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

El patrón es bastante repetitiva que podría valer la pena una macro para evitar errores tipográficos

La inicialización agregada no es un problema, pero de nuevo la sintaxis = default tampoco está disponible.


agradecimiento especial a las personas que ayudaron a mejorar esta respuesta lo largo de los años:

  • Luc Touraille, para señalar a mí en los comentarios que class KeyFoo: boost::noncopyable { friend class Foo; KeyFoo() {} }; desactiva por completo el constructor de copia y de este modo sólo funciona en la variante de delegación (impidiendo el almacenamiento de la instancia).
  • K-ballo, para señalar la forma en C++ 11 mejoró la situación con friend T;
+0

Simple y al grano, me gusta. –

+0

Tengo que preguntarme por qué está pasando el FooKey por referencia. ;) Parece una cosa extraña que hacer con un objeto vacío, sin usar. Aparte de eso, me gusta la idea, y he hecho lo mismo antes de manera ad-hoc, pero esta es la primera vez que realmente lo considero un patrón general. +1 – jalf

+0

Estoy tratando de hacer que la interfaz de Foo muestre y haga cumplir el comportamiento de auto restricción, pero parece que esta técnica solo espera que un 'FooKey &' no se pase a algún método de llamada que no sea confiable para ser competente, no malicioso, y suficientemente justificado para hacer llamadas restringidas. ¿Hay alguna ventaja en este estilo sobre el idioma Attourney-Client vinculado a arriba? – Jeff

1

Sé que esto es una vieja pregunta, pero el problema sigue siendo relevante. Si bien me gusta la idea de la expresión idiomática de abogado-cliente, quería una interfaz transparente para las clases de clientes a las que se les había otorgado acceso privado (o protegido).

Imagino que ya se ha hecho algo similar a esto, pero una mirada superficial no dio ningún resultado. El siguiente método (C++ 11 arriba) funciona por clase (no por objeto) y utiliza una clase base CRTP que es utilizada por la 'clase privada' para exponer un functor público. Solo aquellas clases a las que se les ha dado acceso específicamente pueden llamar al operador del functor(), que luego invoca directamente el método privado asociado a través de una referencia almacenada.

No hay una sobrecarga de llamada de función y la única sobrecarga de memoria es una referencia por método privado que requiere exposición. El sistema es muy versátil; cualquier firma de función y tipo de devolución está permitida, como lo es llamar funciones virtuales en la clase privada.

Para mí, el beneficio principal es uno de sintaxis. Si bien se requiere una declaración ciertamente fea de los objetos del funtor en la clase privada, esto es completamente transparente para las clases de los clientes. He aquí un ejemplo tomado de la pregunta original: clase base

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); 
    }; 
}; 

El GranularPrivacy funciona mediante la definición de 3 clases de plantilla anidadas. El primero de ellos, 'Firma', toma el tipo de retorno de función y la firma de función como parámetros de plantilla, y los reenvía al método operator() del functor y a la segunda clase de plantilla de anidación, 'Function'. Esto se parametriza mediante un puntero a una función miembro privada de la clase Host, que debe tener la firma provista por la clase Signature. En la práctica, se usan dos clases de 'Función' separadas; el que se da aquí, y el otro para las funciones const, se omite por brevedad.

Finalmente, la clase Permitir hereda recursivamente de una clase base explícitamente instanciada utilizando el mecanismo de plantilla variadic, dependiendo del número de clases especificadas en su lista de argumentos de plantilla. Cada nivel de herencia de Permitir tiene un amigo de la lista de plantillas, y las instrucciones de uso elevan el constructor de la clase base y el operador() a la jerarquía de herencia en el ámbito más derivado.

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(); 
      }; 
     }; 
    }; 
}; 

Espero que alguien lo encuentre útil, cualquier comentario o sugerencia sería bienvenido. Esto definitivamente todavía está en progreso. Me gustaría combinar las clases de Firma y Función en una sola clase de plantilla, pero he estado luchando por encontrar una manera de hacerlo. Se pueden encontrar ejemplos más completos y ejecutables en cpp.sh/6ev45 y cpp.sh/2rtrj.

0

He escrito una mejora menor a la solución dada por Matthieu M. La limitación de esta solución es que solo se puede otorgar acceso a una sola clase. ¿Qué sucede si quiero permitir que una de las tres clases tenga acceso?

#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 
    } 
}; 

Este utiliza un enfoque ligeramente diferente para hacer no el objeto un agregado. En lugar de tener un constructor proporcionado por el usuario, tenemos una clase base privada vacía. En la práctica, probablemente no importe, pero significa que esta implementación es una clase POD porque sigue siendo trivial. El efecto debe seguir siendo el mismo, sin embargo, porque nadie va a almacenar estos objetos de todos modos.