2011-05-18 22 views
13

Esa es mi pregunta. Solo tengo curiosidad por saber cuál es el consenso sobre la limitación de los tipos que se pueden pasar a una clase o función genérica. Pensé que había leído en algún momento, que si estaba haciendo programación genérica, generalmente era mejor dejar las cosas abiertas en lugar de tratar de cerrarlas (no recuerde la fuente).Prácticas recomendadas de programación genérica/de plantilla: para limitar tipos, o no para limitar tipos

Estoy escribiendo una biblioteca que tiene algunas funciones genéricas internas, y creo que solo deben permitir el uso de tipos dentro de la biblioteca, simplemente porque así es como quiero que se utilicen. Por otro lado, no estoy seguro de que mi esfuerzo por cerrar las cosas valga la pena.

¿Alguien puede tener algunas fuentes de estadísticas o comentarios autorizados sobre este tema? También estoy interesado en opiniones sensatas. Esperemos que eso no invalide por completo esta pregunta: \

Además, ¿hay etiquetas aquí en SO que equivalgan a "mejores prácticas"? No vi ese específicamente, pero parece que sería útil poder mostrar toda la información de las mejores prácticas para un tema SO dado ... tal vez no, solo un pensamiento.

Editar: Una respuesta hasta el momento menciona que el tipo de biblioteca que estoy haciendo sería significativo. Es una biblioteca de base de datos que termina trabajando con contenedores STL, variadics (tupla), Boost Fusion, cosas de esa naturaleza. Puedo ver cómo eso sería relevante, pero también me interesarían las reglas generales para determinar qué camino tomar.

+2

"si está haciendo programación genérica, generalmente era mejor dejar las cosas abiertas en lugar de tratar de cerrarlas": esto es muy debatible. Pero dado que la gente de C++ decidió que los conceptos no formarían parte del lenguaje en los próximos años (¿décadas?), Es mucho más conveniente dejar las cosas abiertas que intentarlas de forma remota. –

+0

@Alexandre Me preguntaba cuándo se mencionarían los conceptos :( –

Respuesta

14

deje siempre que lo más abierto posible - pero asegúrese de

  • documento de la interfaz necesaria y el comportamiento de los tipos válidos para usarlos con el código genérico.
  • utilice las características de interfaz de un tipo (rasgos) para determinar si se permite o no. No base su decisión en el nombre del tipo.
  • producen un diagnóstico razonable si alguien usa un tipo incorrecto. Las plantillas de C++ son excelentes para generar toneladas de errores profundamente anidados si se instancian con los tipos incorrectos, con características de tipo, afirmaciones estáticas y técnicas relacionadas, uno puede producir fácilmente mensajes de error más sucintos.
+0

Excelente respuesta, pero el último punto no siempre es trivial de implementar. –

+2

@James: con C++ 0x 'static_assert' lo hace mucho más fácil. –

+0

también puedes use 'BOOST_MPL_ASSERT_MSG' si no desea confiar en' static_assert'. Además, el siguiente enlace contiene algunos consejos y sugerencias para reducir el número de errores que puede causar un tipo incorrecto. http://cpp-next.com/archive/2010/09/expressive-c-why-template-errors-suck-and-what-you-can-do-it/ – KitsuneYMG

3

Es uno de los puntos de venta más fuertes de la STL que es tan abierta, y que sus algoritmos trabajan con estructuras mis datos, así como con la que se proporciona en sí, y que mis algoritmos trabajan con sus estructuras de datos como así como con el mío

Si tiene sentido dejar sus algoritmos abiertos a todos los tipos o limitarlos al suyo depende en gran medida de la biblioteca que está escribiendo, de la que no sabemos nada.

(Inicialmente quise responder que ser completamente abierto es de lo que se trata la Programación Genérica, pero ahora veo que siempre hay límites a la genérico, y que hay que trazar la línea en algún lado. Puede que también sea limitado a sus tipos, si eso tiene sentido.)

+0

He publicado una edición de mi pregunta en la parte inferior. Es una biblioteca de base de datos. trabajando con STL, tuplas, Boost Fusion, mucha creatividad aquí: D Una pregunta para mí es, ¿cómo determinar dónde dibujar esa línea? –

+0

Este comentario me hace recordar, el libro de David y Goliat de Malcolm Gladwell, capítulo límite del poder :). –

2

Al menos IMO, lo correcto es más o menos lo que intentaron los conceptos: en lugar de intentar verificar que está recibiendo el tipo especificado (o uno del conjunto de tipos especificados), haga lo posible para especificar el requisitos sobre el tipo, y verifique que el tipo que recibió tenga las características correctas y pueda cumplir los requisitos de su plantilla.

Al igual que con los conceptos, gran parte de la motivación para eso es simplemente proporcionar buenos y útiles mensajes de error cuando no se cumplen esos requisitos. En última instancia, el compilador dará producirá un mensaje de error si alguien intenta crear una instancia de su plantilla sobre un tipo que no cumpla con sus requisitos. El problema es que, con la misma probabilidad, el mensaje de error no será muy útil a menos que tome medidas para asegurarse de que así sea.

3

En mi marco de base de datos, decidí renunciar a las plantillas y usar una sola clase base. La programación genérica significa que cualquier o todos los objetos se pueden utilizar. Las clases de tipos específicos superaron las pocas operaciones genéricas. Por ejemplo, las cadenas y los números se pueden comparar por igualdad; Los BLOB (objetos grandes binarios) pueden desear utilizar un método diferente (como comparar sumas de control MD5 almacenadas en un registro diferente).

Además, había una rama de herencia entre cadenas y tipos numéricos.

Al utilizar una jerarquía de herencia, puedo hacer referencia a cualquier campo mediante la clase Field o una clase especializada como Field_Int.

+0

La programación genérica no es solo tiempo de compilación, como sabiamente señala. –

2

El problema

Si ustedes, los clientes pueden ver sus funciones internas en las cabeceras públicas, y si los nombres de estas funciones genéricas internos son "común", entonces se le puede poner a sus clientes en riesgo de llamar por accidente sus funciones genéricas internas.

Por ejemplo:

namespace Database 
{ 

// internal API, not documented 
template <class DatabaseItem> 
void 
store(DatabaseItem); 
{ 
    // ... 
} 

struct SomeDataBaseType {}; 

} // Database 

namespace ClientCode 
{ 

template <class T, class U> 
struct base 
{ 
}; 

// external API, documented 
template <class T, class U> 
void 
store(base<T, U>) 
{ 
    // ... 
} 

template <class T, class U> 
struct derived 
    : public base<T, U> 
{ 
}; 

} // ClientCode 

int main() 
{ 
    ClientCode::derived<int, Database::SomeDataBaseType> d; 
    store(d); // intended ClientCode::store 
} 

En este ejemplo, el autor de main ni siquiera sabe bases de datos :: existe tienda. Tiene la intención de llamar a ClientCode :: store, y se vuelve perezoso, dejando que ADL elija la función en lugar de especificar ClientCode::store. Después de todo, su argumento a store proviene del mismo espacio de nombres como store, por lo que debería funcionar.

No funciona. Este ejemplo llama al Database::store. Dependiendo de las entrañas de Database::store, esta llamada puede dar como resultado un error de tiempo de compilación, o peor aún, un error de tiempo de ejecución.

Cómo corregir

El más genéricamente designa el nombre de funciones, más probable es que esto va a suceder. Dale a tus funciones internas (las que deben aparecer en tus encabezados) nombres realmente no genéricos. O póngalos en un espacio de nombres secundario como details. En este último caso, debe asegurarse de que sus clientes no tengan el details como espacio de nombres asociado a efectos de ADL. Esto generalmente se logra al no crear tipos que el cliente utilizará, ya sea directa o indirectamente, en namespace details.

Si desea obtener más paranoico, comience a bloquear las cosas con enable_if.

Si tal vez piensas que tus funciones internas pueden ser útiles para tus clientes, entonces ya no son internas.

El código de ejemplo anterior no es exagerado. Me ha pasado esto. Ha sucedido con funciones en namespace std. Llamo al store en este ejemplo excesivamente genérico. std::advance y std::distance son ejemplos clásicos de código excesivamente genérico. Es algo contra lo que debemos protegernos. Y es un problema que los conceptos intentaron solucionar.

+0

Gracias, este es un escenario muy similar a lo que estaba tratando de proteger, aunque no estaba seguro si estaba perdiendo el tiempo. –

Cuestiones relacionadas