Quiero decir, aparte de su nombre obligatorio (la Biblioteca de plantillas estándar) ...
C ++ inicialmente tenía la intención de presentar los conceptos de OOP en C. Es decir: se podría decir lo que una entidad específica podría y no podría hacer (independientemente de cómo lo haga) en función de su clase y jerarquía de clases. Algunas composiciones de habilidades son más difíciles de describir de esta manera debido a la problemática de la herencia múltiple, y al hecho de que C ++ admite el concepto de interfaces de una manera algo torpe (en comparación con Java, etc.), pero está ahí (y podría ser mejorado).
Y luego las plantillas entraron en juego, junto con el STL. El STL parecía tomar los conceptos clásicos de OOP y tirarlos por el desagüe, usando plantillas en su lugar.
Debería haber una distinción entre los casos en que las plantillas se usan para generalizar tipos en los que los tipos mismos son irrelevantes para el funcionamiento de la plantilla (contenedores, por ejemplo). Tener un vector<int>
tiene mucho sentido.
Sin embargo, en muchos otros casos (iteradores y algoritmos), se supone que los tipos con plantilla deben seguir un "concepto" (Input Iterator, Forward Iterator, etc ...) donde los detalles reales del concepto se definen completamente por la implementación de la plantilla función / clase, y no por la clase del tipo utilizado con la plantilla, que es algo anti-uso de OOP.
Por ejemplo, puedes decir la función:
void MyFunc(ForwardIterator<...> *I);
Actualización: como no estaba claro en la pregunta original, ForwardIterator está bien para crear una plantilla para permitir cualquier tipo de ForwardIterator. Lo contrario es tener ForwardIterator como concepto.
espera un iterador de reenvío solo al ver su definición, donde necesitaría mirar la implementación o la documentación para:
template <typename Type> void MyFunc(Type *I);
Dos afirmaciones que puedo hacer a favor del uso de plantillas: el código compilado se puede hacer más eficiente, compilando la plantilla a medida para cada tipo usado, en lugar de usar vtables. Y el hecho de que las plantillas se pueden usar con tipos nativos.
Sin embargo, estoy buscando una razón más profunda por la que abandonar la POO clásica en favor de la plantilla para el STL. (Suponiendo que hayas leído hasta aquí: P)
vector<int>
yvector<char>
para ser utilizado al mismo tiempo. Podrían, sin duda, pero es posible utilizar cualquier dos piezas de código al mismo tiempo. Eso no tiene nada que ver con plantillas, C ++ o STL. No hay nada en la creación de instanciasvector<int>
que requieravector<char>
que se cargue o ejecute código.Respuestas:
La respuesta corta es "porque C ++ ha avanzado". Sí, a finales de los años 70, Stroustrup tenía la intención de crear una C mejorada con capacidades OOP, pero eso fue hace mucho tiempo. Cuando el idioma se estandarizó en 1998, ya no era un idioma OOP. Era un lenguaje multi-paradigmático. Ciertamente tenía cierto soporte para el código OOP, pero también tenía un lenguaje de plantillas completo durante el tiempo superpuesto, permitía la metaprogramación en tiempo de compilación y la gente había descubierto programación genérica. De repente, la POO simplemente no parecía tan importante. No cuando podemos escribir código más simple, más conciso y más eficiente mediante el uso de técnicas disponibles a través de plantillas y programación genérica.
OOP no es el santo grial. Es una idea linda, y fue una gran mejora con respecto a los lenguajes de procedimiento en los años 70 cuando se inventó. Pero, sinceramente, no todo es tan bueno como parece. En muchos casos es torpe y detallado y no promueve realmente código reutilizable o modularidad.
Es por eso que la comunidad C ++ está hoy mucho más interesada en la programación genérica, y por qué todos finalmente comienzan a darse cuenta de que la programación funcional también es bastante inteligente. OOP por sí solo no es una vista bonita.
Intente dibujar un gráfico de dependencia de un hipotético STL "OOP-ified". ¿Cuántas clases tendrían que saber unos de otros? Habría muchas dependencias. ¿Sería capaz de incluir solo el
vector
encabezado, sin obteneriterator
o inclusoiostream
extraer? El STL lo hace fácil. Un vector sabe sobre el tipo de iterador que define, y eso es todo. Los algoritmos STL no saben nada . Ni siquiera necesitan incluir un encabezado de iterador, a pesar de que todos aceptan iteradores como parámetros. ¿Cuál es más modular entonces?El STL puede no seguir las reglas de OOP como Java lo define, pero ¿no logra los objetivos de OOP? ¿No logra la reutilización, el bajo acoplamiento, la modularidad y la encapsulación?
¿Y no logra estos objetivos mejor que una versión con OOP?
En cuanto a por qué el STL fue adoptado en el lenguaje, sucedieron varias cosas que condujeron al STL.
Primero, se agregaron plantillas a C ++. Se agregaron por la misma razón que los genéricos se agregaron a .NET. Parecía una buena idea poder escribir cosas como "contenedores de un tipo T" sin tirar la seguridad del tipo. Por supuesto, la implementación que adoptaron fue bastante más compleja y poderosa.
Luego, la gente descubrió que el mecanismo de plantilla que habían agregado era aún más poderoso de lo esperado. Y alguien comenzó a experimentar con el uso de plantillas para escribir una biblioteca más genérica. Uno inspirado en la programación funcional, y uno que utilizó todas las nuevas capacidades de C ++.
Lo presentó al comité de lenguaje C ++, que tardó bastante en acostumbrarse porque parecía muy extraño y diferente, pero finalmente se dio cuenta de que funcionaba mejor que los equivalentes tradicionales de OOP que tendrían que incluir de otra manera . Entonces hicieron algunos ajustes y lo adoptaron en la biblioteca estándar.
No fue una elección ideológica, no fue una elección política de "queremos ser OOP o no", sino muy pragmática. Evaluaron la biblioteca y vieron que funcionaba muy bien.
En cualquier caso, las dos razones que mencionas para favorecer el STL son absolutamente esenciales.
La biblioteca estándar de C ++ tiene que ser eficiente. Si es menos eficiente que, digamos, el código C enrollado a mano equivalente, entonces la gente no lo usaría. Eso reduciría la productividad, aumentaría la probabilidad de errores y, en general, sería una mala idea.
Y el STL tiene que trabajar con tipos primitivos, porque los tipos primitivos son todo lo que tienes en C, y son una parte importante de ambos lenguajes. Si el STL no funcionara con matrices nativas, sería inútil .
Su pregunta tiene una fuerte suposición de que la POO es "la mejor". Tengo curiosidad por saber por qué. Usted pregunta por qué "abandonaron la OOP clásica". Me pregunto por qué deberían haberse quedado con eso. ¿Qué ventajas habría tenido?
fuente
std::set
como ejemplo. No hereda de una clase base abstracta. ¿Cómo limita eso su uso destd::set
? ¿Hay algo que no pueda hacer con unstd::set
porque no hereda de una clase base abstracta?La respuesta más directa a lo que creo que está preguntando / quejándose es esta: la suposición de que C ++ es un lenguaje OOP es una suposición falsa.
C ++ es un lenguaje multi-paradigmático. Se puede programar utilizando los principios de OOP, se puede programar de manera procesal, se puede programar genéricamente (plantillas) y con C ++ 11 (anteriormente conocido como C ++ 0x) algunas cosas se pueden programar funcionalmente.
Los diseñadores de C ++ ven esto como una ventaja, por lo que argumentan que limitar C ++ para actuar como un lenguaje puramente OOP cuando la programación genérica resuelve mejor el problema y, bueno, más genéricamente , sería un paso atrás.
fuente
Tengo entendido que Stroustrup originalmente prefería un diseño de contenedor de "estilo OOP" y, de hecho, no veía otra forma de hacerlo. Alexander Stepanov es el responsable de la STL, y sus objetivos no incluían "hacerlo orientado a objetos" :
(Él explica por qué la herencia y los virtuales, también conocido como diseño orientado a objetos "era fundamentalmente defectuoso y no debería usarse" en el resto de la entrevista).
Una vez que Stepanov presentó su biblioteca a Stroustrup, Stroustrup y otros realizaron esfuerzos hercúleos para llevarla al estándar ISO C ++ (misma entrevista):
fuente
La respuesta se encuentra en esta entrevista con Stepanov, el autor de la STL:
fuente
¿Por qué sería mejor un diseño OOP puro para una Biblioteca de Estructura de Datos y Algoritmos? OOP no es la solución para todo.
En mi humilde opinión, STL es la biblioteca más elegante que he visto :)
para su pregunta
no necesita un polimorfismo de tiempo de ejecución, es una ventaja para STL implementar realmente la Biblioteca usando polimorfismo estático, eso significa eficiencia. ¡Intente escribir una clasificación o distancia genérica o el algoritmo que se aplique a TODOS los contenedores! ¡Su Sort in Java llamaría a funciones que son dinámicas a través de n niveles para ser ejecutadas!
Necesitas cosas estúpidas como Boxing y Unboxing para ocultar suposiciones desagradables de los llamados lenguajes Pure OOP.
El único problema que veo con STL, y las plantillas en general son los horribles mensajes de error. Que se resolverá utilizando Conceptos en C ++ 0X.
Comparar STL con Colecciones en Java es como comparar Taj Mahal con mi casa :)
fuente
static_assert
quizás.Creo que malinterpretas el uso previsto de los conceptos por parte de las plantillas. Forward Iterator, por ejemplo, es un concepto muy bien definido. Para encontrar las expresiones que deben ser válidas para que una clase sea un iterador directo, y su semántica, incluida la complejidad computacional, consulte el estándar o http://www.sgi.com/tech/stl/ForwardIterator.html (tiene que seguir los enlaces a Entrada, Salida e Iterador trivial para verlo todo).
Ese documento es una interfaz perfectamente buena, y "los detalles reales del concepto" se definen allí mismo. No están definidos por las implementaciones de Forward Iterators, y tampoco están definidos por los algoritmos que usan Forward Iterators.
Las diferencias en cómo se manejan las interfaces entre STL y Java son triples:
1) STL define expresiones válidas utilizando el objeto, mientras que Java define métodos que deben ser invocables en el objeto. Por supuesto, una expresión válida podría ser una llamada a método (función miembro), pero no tiene que ser así.
2) Las interfaces Java son objetos de tiempo de ejecución, mientras que los conceptos de STL no son visibles en tiempo de ejecución incluso con RTTI.
3) Si no puede hacer válidas las expresiones válidas requeridas para un concepto STL, obtendrá un error de compilación no especificado cuando crea una instancia de alguna plantilla con el tipo. Si no puede implementar un método requerido de una interfaz Java, recibirá un error de compilación específico que lo indica.
Esta tercera parte es si desea una especie de "tipeo de pato" (tiempo de compilación): las interfaces pueden ser implícitas. En Java, las interfaces son algo explícitas: una clase "es" Iterable si y solo si dice que implementa Iterable. El compilador puede verificar que las firmas de sus métodos estén todas presentes y sean correctas, pero la semántica aún está implícita (es decir, están documentadas o no, pero solo más código (pruebas unitarias) pueden decirle si la implementación es correcta).
En C ++, como en Python, tanto la semántica como la sintaxis están implícitas, aunque en C ++ (y en Python si obtienes el preprocesador de tipo fuerte) obtienes ayuda del compilador. Si un programador requiere una declaración explícita de interfaces similar a Java por parte de la clase implementadora, entonces el enfoque estándar es usar rasgos de tipo (y la herencia múltiple puede evitar que esto sea demasiado detallado). Lo que falta, en comparación con Java, es una plantilla única que puedo instanciar con mi tipo, y que compilará si y solo si todas las expresiones requeridas son válidas para mi tipo. Esto me diría si he implementado todos los bits requeridos, "antes de usarlo". Eso es una conveniencia, pero no es el núcleo de OOP (y todavía no prueba la semántica,
STL puede o no ser lo suficientemente OO para su gusto, pero ciertamente separa la interfaz limpiamente de la implementación. Carece de la capacidad de Java para hacer reflexiones sobre las interfaces, y reporta infracciones de los requisitos de la interfaz de manera diferente.
Personalmente, creo que los tipos implícitos son una fortaleza, cuando se usan adecuadamente. El algoritmo dice lo que hace con sus parámetros de plantilla, y el implementador se asegura de que esas cosas funcionen: es exactamente el denominador común de lo que deberían hacer las "interfaces". Además, con STL, es poco probable que utilice, por ejemplo,
std::copy
basándose en encontrar su declaración de reenvío en un archivo de encabezado. Los programadores deberían determinar qué toma una función en función de su documentación, no solo de la firma de la función. Esto es cierto en C ++, Python o Java. Existen limitaciones sobre lo que se puede lograr con la escritura en cualquier idioma, y tratar de usar la escritura para hacer algo que no hace (verificar semántica) sería un error.Dicho esto, los algoritmos STL generalmente nombran sus parámetros de plantilla de una manera que deja en claro qué concepto se requiere. Sin embargo, esto es para proporcionar información adicional útil en la primera línea de la documentación, no para hacer declaraciones futuras más informativas. Hay más cosas que necesita saber que se pueden encapsular en los tipos de parámetros, por lo que debe leer los documentos. (Por ejemplo, en algoritmos que toman un rango de entrada y un iterador de salida, lo más probable es que el iterador de salida necesite suficiente "espacio" para un cierto número de salidas en función del tamaño del rango de entrada y tal vez los valores en él. Intente escribirlo con firmeza. )
Aquí está Bjarne en las interfaces declaradas explícitamente: http://www.artima.com/cppsource/cpp0xP.html
Mirándolo al revés, con pato escribiendo puede implementar una interfaz sin saber que la interfaz existe. O alguien puede escribir una interfaz deliberadamente para que su clase la implemente, después de consultar sus documentos para ver que no piden nada que usted no haya hecho. Eso es flexible
fuente
std
biblioteca que no coincide con un concepto suele estar "mal formado, no se requiere diagnóstico"."OOP para mí significa solo mensajes, retención local y protección y ocultación del proceso de estado, y un enlace tardío extremo de todas las cosas. Se puede hacer en Smalltalk y en LISP. Posiblemente hay otros sistemas en los que esto es posible, pero No estoy al tanto de ellos ". - Alan Kay, creador de Smalltalk.
C ++, Java y la mayoría de los otros lenguajes están bastante lejos de la OOP clásica. Dicho esto, defender las ideologías no es terriblemente productivo. C ++ no es puro en ningún sentido, por lo que implementa una funcionalidad que parece tener sentido pragmático en ese momento.
fuente
STL comenzó con la intención de proporcionar una gran biblioteca que cubra el algoritmo más utilizado, con el objetivo de un comportamiento y rendimiento consistentes . La plantilla fue un factor clave para hacer que esa implementación y el objetivo sean factibles.
Solo para proporcionar otra referencia:
Al Stevens entrevista a Alex Stepanov, en marzo de 1995, de DDJ:
Stepanov explicó su experiencia laboral y la elección realizada hacia una gran biblioteca de algoritmos, que finalmente evolucionó a STL.
fuente
El problema básico con
¿Cómo se obtiene de forma segura el tipo de cosa que devuelve el iterador? Con las plantillas, esto se hace por usted en tiempo de compilación.
fuente
Por un momento, pensemos en la biblioteca estándar como básicamente una base de datos de colecciones y algoritmos.
Si ha estudiado la historia de las bases de datos, indudablemente sabe que desde el principio, las bases de datos eran principalmente "jerárquicas". Las bases de datos jerárquicas se correspondían muy estrechamente con la OOP clásica, específicamente, la variedad de herencia única, como la utilizada por Smalltalk.
Con el tiempo, se hizo evidente que las bases de datos jerárquicas podían usarse para modelar casi cualquier cosa, pero en algunos casos el modelo de herencia única era bastante limitante. Si tenía una puerta de madera, era útil poder mirarla como una puerta o como una pieza de alguna materia prima (acero, madera, etc.)
Entonces, inventaron las bases de datos del modelo de red. Las bases de datos del modelo de red se corresponden muy estrechamente con la herencia múltiple. C ++ admite la herencia múltiple por completo, mientras que Java admite una forma limitada (puede heredar de una sola clase, pero también puede implementar tantas interfaces como desee).
Tanto el modelo jerárquico como las bases de datos del modelo de red se han desvanecido en su mayoría por el uso general (aunque algunos permanecen en nichos bastante específicos). Para la mayoría de los propósitos, han sido reemplazados por bases de datos relacionales.
Gran parte de la razón por la que las bases de datos relacionales se hicieron cargo fue la versatilidad. El modelo relacional es funcionalmente un superconjunto del modelo de red (que, a su vez, es un superconjunto del modelo jerárquico).
C ++ ha seguido en gran medida el mismo camino. La correspondencia entre la herencia única y el modelo jerárquico y entre la herencia múltiple y el modelo de red es bastante obvia. La correspondencia entre las plantillas de C ++ y el modelo jerárquico puede ser menos obvia, pero de todos modos es bastante adecuada.
No he visto una prueba formal de ello, pero creo que las capacidades de las plantillas son un superconjunto de las proporcionadas por la herencia múltiple (que es claramente un superconjunto de inercia única). La única parte difícil es que las plantillas están en su mayoría enlazadas estáticamente, es decir, todo el enlace ocurre en tiempo de compilación, no en tiempo de ejecución. Como tal, una prueba formal de que la herencia proporciona un superconjunto de las capacidades de la herencia puede ser algo difícil y complejo (o incluso imposible).
En cualquier caso, creo que esa es la razón principal por la que C ++ no usa la herencia para sus contenedores; no hay una razón real para hacerlo, porque la herencia proporciona solo un subconjunto de las capacidades proporcionadas por las plantillas. Dado que las plantillas son básicamente una necesidad en algunos casos, también podrían usarse en casi todas partes.
fuente
¿Cómo se hacen comparaciones con ForwardIterator *? Es decir, ¿cómo verifica si el artículo que tiene es lo que está buscando o lo ha pasado por alto?
La mayoría de las veces, usaría algo como esto:
lo que significa que sé que estoy apuntando a MyType's y sé cómo compararlos. Aunque parece una plantilla, en realidad no lo es (sin palabra clave "plantilla").
fuente
Esta pregunta tiene muchas respuestas geniales. También debe mencionarse que las plantillas admiten un diseño abierto. Con el estado actual de los lenguajes de programación orientados a objetos, uno tiene que usar el patrón de visitante cuando se trata de tales problemas, y la verdadera OOP debe admitir el enlace dinámico múltiple. Ver Open Multi-Methods para C ++, P. Pirkelbauer, et.al. para lectura muy interesante.
Otro punto interesante de las plantillas es que también se pueden usar para el polimorfismo de tiempo de ejecución. Por ejemplo
Tenga en cuenta que esta función también funcionará si
Value
es un vector de algún tipo ( no std :: vector, que debe llamarsestd::dynamic_array
para evitar confusiones)Si
func
es pequeño, esta función ganará mucho con la alineación. Ejemplo de usoEn este caso, debe saber la respuesta exacta (2.718 ...), pero es fácil construir un ODE simple sin solución elemental (Sugerencia: use un polinomio en y).
Ahora, tiene una expresión grande
func
y usa el solucionador ODE en muchos lugares, por lo que su ejecutable se contamina con instancias de plantillas en todas partes. ¿Qué hacer? Lo primero que debe notar es que funciona un puntero de función regular. Luego desea agregar curry para escribir una interfaz y una instanciación explícitaPero la instanciación anterior solo funciona
double
, ¿ por qué no escribir la interfaz como plantilla?y se especializan para algunos tipos de valores comunes:
Si la función se hubiera diseñado primero en torno a una interfaz, entonces se habría visto obligado a heredar de ese ABC. Ahora tiene esta opción, así como el puntero de función, lambda o cualquier otro objeto de función. La clave aquí es que debemos tener
operator()()
, y debemos poder usar algunos operadores aritméticos en su tipo de retorno. Por lo tanto, la maquinaria de la plantilla se rompería en este caso si C ++ no tuviera sobrecarga del operador.fuente
El concepto de separar la interfaz de la interfaz y poder intercambiar las implementaciones no es intrínseco a la Programación Orientada a Objetos. Creo que es una idea que surgió en el Desarrollo basado en componentes como Microsoft COM. (Vea mi respuesta en ¿Qué es el desarrollo impulsado por componentes?) Al crecer y aprender C ++, a las personas se les promocionó la herencia y el polimorfismo. No fue hasta que la gente de los años 90 comenzó a decir "Programar a una 'interfaz', no una 'implementación'" y "Favorecer 'composición de objetos' sobre 'herencia de clase'". (ambos citados de GoF por cierto).
Luego, Java llegó junto con el recolector de basura incorporado y la
interface
palabra clave, y de repente se hizo práctico separar la interfaz y la implementación. Antes de que te des cuenta, la idea se convirtió en parte de la OO. C ++, plantillas y STL son anteriores a todo esto.fuente