c++ - ¿Podemos aumentar la reutilización de este patrón de protección de acceso orientado a las teclas?
design-patterns idioms (3)
¿Podemos aumentar la reutilización para este patrón de protección de acceso orientado a claves ?
class SomeKey {
friend class Foo;
// more friends... ?
SomeKey() {}
// possibly non-copyable too
};
class Bar {
public:
void protectedMethod(SomeKey); // only friends of SomeKey have access
};
Para evitar malentendidos continuos, este patrón es diferente del idioma del Attorney-Client :
- Puede ser más conciso que Attorney-Client (ya que no implica el proxy a través de una tercera clase)
- Puede permitir la delegación de derechos de acceso
- ... pero también es más intrusivo en la clase original (un parámetro ficticio por método)
(Una discusión lateral desarrollada en esta pregunta , así que estoy abriendo esta pregunta).
Gran respuesta de @GManNickG. Aprendí mucho Al tratar de hacer que funcione, encontré un par de errores tipográficos. Ejemplo completo repetido para mayor claridad. Mi ejemplo toma prestada la función "contiene claves en las teclas ..." de Comprobar si el paquete de parámetros C ++ 0x contiene un tipo publicado por @snk_kid.
#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;
}
He leído muchos comentarios sobre la no copiabilidad. Mucha gente pensó que no debería ser copiable porque no podemos pasarlo como argumento a la función que necesita la clave. Y algunos incluso se sorprendieron de que funcionara. Bueno, realmente no debería y está aparentemente relacionado con algunos compiladores de Visual C ++, ya que tenía la misma rareza antes pero no con Visual C ++ 12 (Studio 2013).
Pero aquí está la cosa, podemos mejorar la seguridad con la no copiabilidad "básica". La versión de Boost es demasiado, ya que impide por completo el uso del constructor de copias y, por lo tanto, es demasiado para lo que necesitamos. Lo que necesitamos es hacer que el constructor de copias sea privado, pero no sin una implementación. Por supuesto, la implementación estará vacía, pero debe existir. Recientemente, me pregunté quién llamaba al copiador en ese caso (en este caso, quién llama al constructor de copia de SomeKey
cuando llama a ProtectedMethod
). La respuesta fue que aparentemente el estándar asegura que es la persona que llama al método la que llama al -ctor
que honestamente parece bastante lógico. Así que al hacer que copy-ctor
privado permitimos que los amigos funcionen (la Bar
protected
y el Foo
granted
) para llamarlo, permitiendo así que Foo
llame a ProtectedMethod
porque usa el argumento de valor pasando, pero también previene a cualquier persona fuera del alcance de Foo
.
Al hacer esto, incluso si un compañero desarrollador intenta jugar inteligentemente con el código, en realidad tendrá que hacer que Foo
haga el trabajo, otra clase no podrá obtener la clave, y es probable que se dé cuenta de sus errores casi 100 % de las veces de esta manera (con suerte, de lo contrario, él es demasiado principiante para usar este patrón o debería detener el desarrollo: P).
Me gusta este modismo, y tiene el potencial de ser mucho más limpio y expresivo.
En el estándar C ++ 03, creo que la forma siguiente es la más fácil de usar y la más genérica. (No demasiada mejora, sin embargo, la mayoría de las veces se ahorra al repetirse). Debido a que los parámetros de la plantilla no pueden ser amigos , tenemos que usar una macro para definir la clave de acceso:
// 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(){}
Este método tiene dos inconvenientes: 1) la persona que llama debe conocer la clave específica que necesita para crear. Si bien un simple esquema de nombres ( function_key
) básicamente lo elimina, podría ser una abstracción más limpia (y más fácil). 2) Si bien no es muy difícil de usar, la macro puede verse como un poco fea, requiriendo un bloque de definiciones de clave de paso. Sin embargo, las mejoras a estos inconvenientes no se pueden realizar en C ++ 03.
En C ++ 0x, el modismo puede alcanzar su forma más simple y más expresiva. Esto se debe tanto a plantillas variadic como a permitir que los parámetros de la plantilla sean amigos. (Tenga en cuenta que MSVC pre-2010 permite especificar amigos de plantilla como una extensión, por lo tanto, se puede simular esta solución):
// 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(){}
Tenga en cuenta que solo con el código repetitivo, en la mayoría de los casos (¡ todos los casos sin función!), Nunca más debe definirse especialmente. Este código implementa de manera genérica y simple la expresión idiomática para cualquier combinación de clases y funciones.
La persona que llama no necesita intentar crear o recordar una clave específica para la función. Más bien, cada clase ahora tiene su propia clave única y la función simplemente elige qué clave de paso permitirá en los parámetros de la plantilla del parámetro clave (no se requieren definiciones adicionales); esto elimina ambos inconvenientes. La persona que llama simplemente crea su propia contraseña y llama con eso, y no necesita preocuparse por nada más.