c++ - Conversión implícita débilmente acoplada
boost-mpl enable-if (6)
¿Hay alguna desventaja en cualquiera de los enfoques? ¿Permitir conversiones como esa es peligroso? ¿Deberían los implementadores de la biblioteca en general suministrar el segundo método cuando ...
En general, hay una desventaja en la conversión implícita que realiza cualquier trabajo, ya que perjudica a los usuarios de la biblioteca que son sensibles a la velocidad (p. Ej., Úselo, quizás sin saberlo, en un bucle interno). También puede causar un comportamiento inesperado cuando hay varias conversiones implícitas diferentes disponibles. Así que diría que sería un mal consejo para los implementadores de bibliotecas en general permitir conversiones implícitas.
En su caso, esencialmente convertir una tupla de números (A) en otra tupla (B), es tan fácil que un compilador puede alinear la conversión y tal vez optimizarla por completo. Así que la velocidad no es un problema. Puede que tampoco haya otras conversiones implícitas para confundir las cosas. Así que la conveniencia bien puede ganar. Pero la decisión de proporcionar una conversión implícita se debe tomar caso por caso, y tales casos serían raros.
Un mecanismo general como el que sugieres con la segunda variante raramente sería útil y facilitaría hacer algunas cosas bastante malas. Toma esto como ejemplo (ideado pero aún):
struct A {
A(float x) : x(x) {}
int x;
};
struct B {
B(int y): y(y) {}
template<class T> B(const T &t) { *this = convert(t); }
int y;
};
inline B convert(const A &a) {
return B(a.x+1);
}
En este caso, deshabilitar el constructor de plantillas cambiará el valor de B (20.0). En otras palabras, simplemente agregando un constructor de conversión implícito, puede cambiar la interpretación del código existente. Obviamente, eso es muy peligroso. Por lo tanto, la conversión implícita no debería estar comúnmente disponible, sino que debe proporcionarse para tipos muy específicos, solo cuando es valiosa y bien entendida. No sería lo suficientemente común como para justificar su segunda variante.
Para resumir: esto se haría mejor fuera de las bibliotecas, con un conocimiento completo de todos los tipos que se admitirán. Un objeto proxy parece perfecto.
La conversión implícita puede ser realmente útil cuando los tipos son semánticamente equivalentes. Por ejemplo, imagine dos bibliotecas que implementan un tipo de forma idéntica, pero en espacios de nombres diferentes. O simplemente un tipo que es casi idéntico, excepto un poco de azúcar semántica aquí y allá. Ahora no puede pasar un tipo a una función (en una de esas bibliotecas) que fue diseñada para usar la otra, a menos que esa función sea una plantilla. Si no es así, tienes que convertir de alguna manera un tipo en el otro. Esto debería ser trivial (o, de lo contrario, ¡los tipos no son tan idénticos después de todo!), Pero la conversión explícita infla su código con llamadas de función mayormente sin sentido. Si bien estas funciones de conversión pueden copiar algunos valores, esencialmente no hacen nada desde un punto de vista de "programadores" de alto nivel.
Obviamente, los constructores y operadores de conversión implícita podrían ayudar, pero introducen el acoplamiento, de modo que uno de esos tipos debe conocer al otro. Generalmente, al menos cuando se trata de bibliotecas, ese no es el caso, porque la presencia de uno de esos tipos hace que el otro sea redundante. Además, no siempre se pueden cambiar las bibliotecas.
Ahora veo dos opciones sobre cómo hacer que la conversión implícita funcione en código de usuario:
El primero sería proporcionar un tipo de proxy, que implemente operadores de conversión y constructores de conversión (y asignaciones) para todos los tipos involucrados, y usarlo siempre.
El segundo requiere un cambio mínimo en las bibliotecas, pero permite una gran flexibilidad: agregue un constructor de conversión para cada tipo involucrado que pueda habilitarse opcionalmente de manera externa.
Por ejemplo, para un tipo A
agregue un constructor:
template <class T> A(
const T& src,
typename boost::enable_if<conversion_enabled<T,A>>::type* ignore=0
)
{
*this = convert(src);
}
y una plantilla
template <class X, class Y>
struct conversion_enabled : public boost::mpl::false_ {};
que deshabilita la conversión implícita por defecto.
Luego, para habilitar la conversión entre dos tipos, especialice la plantilla:
template <> struct conversion_enabled<OtherA, A> : public boost::mpl::true_ {};
e implementar una función de convert
que se puede encontrar a través de ADL.
Personalmente preferiría usar la segunda variante, a menos que haya fuertes argumentos en contra.
Ahora a la (s) pregunta (s) real (es): ¿Cuál es la forma preferida de asociar tipos para la conversión implícita? ¿Son mis sugerencias buenas ideas? ¿Hay alguna desventaja en cualquiera de los enfoques? ¿Permitir conversiones como esa es peligroso? En caso de que los implementadores de bibliotecas en general proporcionen el segundo método cuando es probable que su tipo se replique en el software con el que más probablemente se usarán (estoy pensando en el software de renderizado 3D aquí, donde la mayoría de esos paquetes implementan un 3D vector).
¿Podrías usar la sobrecarga del operador de conversión? Como en el siguiente ejemplo:
class Vector1 {
int x,y,z;
public:
Vector1(int x, int y, int z) : x(x), y(y), z(z) {}
};
class Vector2 {
float x,y,z;
public:
Vector2(float x, float y, float z) : x(x), y(y), z(z) {}
operator Vector1() {
return Vector1(x, y, z);
}
};
Ahora estas llamadas tienen éxito:
void doIt1(const Vector1 &v) {
}
void doIt2(const Vector2 &v) {
}
Vector1 v1(1,2,3);
Vector2 v2(3,4,5);
doIt1(v1);
doIt2(v2);
doIt1(v2); // Implicitely convert Vector2 into Vector1
Hoy estoy lento ¿Cuál fue el problema con el uso del patrón proxy de nuevo? Mi consejo es que no dedique demasiado tiempo a preocuparse por las funciones de copia que realizan un trabajo innecesario. Además, lo explícito es bueno.
Podría escribir una clase de convertidor (algún proxy) que implícitamente puede convertir desde y hacia los tipos incompatibles. Luego, puede usar un constructor para generar el proxy de uno de los tipos y pasarlo al método. El proxy devuelto se convertiría directamente al tipo deseado.
El inconveniente es que tiene que ajustar el parámetro en todas las llamadas. Bien hecho, el compilador incluso integrará la llamada completa sin crear una instancia del proxy. Y no hay acoplamiento entre las clases. Solo las clases de Proxy necesitan conocerlas.
Ha pasado un tiempo desde que programé C ++, pero el proxy debería ser algo como esto:
class Proxy {
private:
IncompatibleType1 *type1;
IncompatibleType2 *type2;
//TODO static conversion methods
public:
Proxy(IncompatibleType1 *type1) {
this.type1=type1;
}
Proxy(IncompatibleType2 *type2) {
this.type2=type2;
}
operator IncompatibleType1 * () {
if(this.type1!=NULL)
return this.type1;
else
return convert(this.type2);
}
operator IncompatibleType2 * () {
if(this.type2!=NULL)
return this.type2;
else
return convert(this.type1);
}
}
Las llamadas siempre se verían como:
expectsType1(Proxy(type2));
expectsType1(Proxy(type1));
expectsType2(Proxy(type1));
Preferiría su enfoque de "proxy" en lugar de otras opciones, si me molesta en absoluto.
La verdad del asunto es que he encontrado que este es un problema tan importante en TODAS las esferas del desarrollo que tiendo a evitar el uso de cualquier construcción específica de biblioteca fuera de mi interacción con esa biblioteca en particular. Un ejemplo podría ser tratar con eventos / señales en varias bibliotecas diferentes. Ya elegí boost como algo que es parte integral de mi propio código de proyecto, por lo que, a propósito, uso boost :: signal2 para todas las comunicaciones dentro de mi propio código de proyecto. Luego escribo interfaces en la biblioteca de interfaz de usuario que estoy usando.
Otro ejemplo son las cadenas. Cada maldita biblioteca de UI reinventa la cadena. Todos mis modelos y código de datos utilizan las versiones estándar y proporciono interfaces a mis envoltorios de IU que funcionan en este tipo ... convirtiendo a la versión específica de UI solo en ese punto donde estoy interactuando directamente con un componente de UI.
Esto significa que no puedo aprovechar una gran cantidad de poder proporcionado por varias construcciones independientes pero similares, y estoy escribiendo un montón de código adicional para lidiar con estas conversiones, pero vale la pena porque si encuentro mejores bibliotecas y / o la necesidad de cambiar de plataforma es mucho más fácil hacerlo ya que no he permitido que estas cosas se desplacen a lo largo de todo.
Básicamente, prefiero el enfoque proxy porque ya lo estoy haciendo. Trabajo en capas abstractas que me alejan de cualquier biblioteca específica que estoy usando y subclasifico esas abstracciones con los detalles necesarios para interactuar con dicha biblioteca. SIEMPRE lo estoy haciendo, así que me pregunto sobre un área pequeña en la que quiero compartir información entre dos bibliotecas de terceros que ya está respondida.
Respecto a tu primera opción:
Proporcione un tipo de proxy, que implemente operadores de conversión y constructores de conversión (y asignaciones) para todos los tipos involucrados, y siempre use eso.
Puede usar cadenas (texto) como proxy, si el rendimiento no es crítico (o tal vez si lo es y los datos son fundamentalmente cadenas). Implemente los operadores <<
y >>
y puede usar boost::lexical_cast<>
para convertir usando una representación intermedia textual:
const TargetType& foo = lexical_cast<TargetType>(bar);
Obviamente, si estás muy preocupado por el rendimiento, no deberías hacer esto, y también hay otras advertencias (ambos tipos deberían tener representaciones de texto sensibles), pero es bastante universal y "simplemente funciona" con muchas cosas existentes.