¿Por qué el STL de C ++ está tan fuertemente basado en plantillas? (y no en * interfaces *)

211

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)

OB OB
fuente
44
Puede consultar stackoverflow.com/questions/31693/… . La respuesta aceptada es una excelente explicación de qué plantillas le ofrecen sobre los genéricos.
James McMahon
66
@Jonas: Eso no tiene sentido. La restricción en el caché cuesta ciclos de reloj, por eso es importante. Al final del día, son los ciclos de reloj, no el caché, los que definen el rendimiento. La memoria y la memoria caché solo son importantes en la medida en que afectan los ciclos de reloj gastados. Además, el experimento se puede hacer fácilmente. Compare, digamos, std :: for_Each llamado con un argumento functor, con el enfoque OOP / vtable equivalente. La diferencia en el rendimiento es asombrosa . Por eso se utiliza la versión de la plantilla.
jalf
77
y no hay ninguna razón por la cual el código redundante estaría llenando el icache. Si ejecuto el vector <char> y el vector <int> en mi programa, ¿por qué debería cargarse el código del vector <char> en icache mientras estoy procesando el vector <int>? De hecho, el código para el vector <int> se recorta porque no tiene que incluir código para conversión, vtables e indirección.
jalf
3
Alex Stepanov explica por qué la herencia y la igualdad no juegan bien juntas.
fredoverflow
66
@BerndJendrissek: Uhm, cerca, pero no a ti mismo. Sí, más costos de código en términos de ancho de banda de memoria y uso de caché si alguna vez se usa realmente . Pero no hay ninguna razón particular para esperar una vector<int>y vector<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 instancias vector<int>que requiera vector<char>que se cargue o ejecute código.
jalf

Respuestas:

607

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 vectorencabezado, sin obtener iteratoro incluso iostreamextraer? 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?

jalf
fuente
22
Es una buena reseña, pero me gustaría resaltar un detalle. El STL no es un "producto" de C ++. De hecho, STL, como concepto, existía antes de C ++, y C ++ resultó ser un lenguaje eficiente que tenía (casi) suficiente potencia para la programación genérica, por lo que STL se escribió en C ++.
Igor Krivokon
17
Dado que los comentarios siguen apareciendo, sí, soy consciente de que el nombre STL es ambiguo. Pero no puedo pensar en un mejor nombre para "la parte de la biblioteca estándar de C ++ que está modelada en el STL". El nombre de facto para esa parte de la biblioteca estándar es simplemente "el STL", aunque es estrictamente inexacto. :) Mientras las personas no usen STL como el nombre de toda la biblioteca estándar (incluidos IOStreams y los encabezados C stdlib), estoy feliz. :)
jalf
55
@einpoklum ¿Y qué ganarías exactamente de una clase base abstracta? Toma std::setcomo ejemplo. No hereda de una clase base abstracta. ¿Cómo limita eso su uso de std::set? ¿Hay algo que no pueda hacer con un std::setporque no hereda de una clase base abstracta?
fredoverflow
22
@einpoklum, por favor, eche un vistazo al lenguaje Smalltalk, que Alan Kay diseñó para ser un lenguaje OOP cuando inventó el término OOP. No tenía interfaces. OOP no se trata de interfaces o clases base abstractas. ¿Va a decir que "Java, que no se parece en nada a lo que el inventor del término OOP tenía en mente, es más OOP que C ++, que tampoco se parece en nada a lo que el inventor del término OOP tenía en mente"? Lo que quiere decir es que "C ++ no es lo suficientemente similar a Java para mi gusto". Eso es justo, pero no tiene nada que ver con OOP.
jalf
8
@MasonWheeler si esta respuesta fuera una tontería descarada, no verías literalmente a cientos de desarrolladores de todo el mundo votando +1 sobre esto con solo tres personas haciendo lo contrario
panda-34
88

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.

Tyler McHenry
fuente
44
"y con C ++ 0x, algunas cosas incluso se pueden programar funcionalmente", se puede programar funcionalmente sin esas características, simplemente de manera más detallada.
Jonas Kölker
3
@Tyler De hecho, si restringió C ++ a POO puro, se quedaría con Objective-C.
Justicle
@TylerMcHenry: ¡Acabo de preguntar esto , me parece que acabo de pronunciar la misma respuesta que tú! Solo un punto. Deseo que agregue el hecho de que la Biblioteca estándar no se puede usar para escribir código orientado a objetos.
einpoklum
74

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" :

Ese es el punto fundamental: los algoritmos se definen en estructuras algebraicas. Me tomó un par de años darme cuenta de que hay que ampliar la noción de estructura agregando requisitos de complejidad a los axiomas regulares. ... Creo que las teorías de iterador son tan centrales para la informática como las teorías de los anillos o los espacios de Banach son fundamentales para las matemáticas. Cada vez que miraba un algoritmo, intentaba encontrar una estructura en la que estuviera definido. Entonces, lo que quería hacer era describir algoritmos genéricamente. Eso es lo que me gusta hacer. Puedo pasar un mes trabajando en un algoritmo conocido tratando de encontrar su representación genérica. ...

STL, al menos para mí, representa la única forma en que la programación es posible. De hecho, es bastante diferente de la programación en C ++, ya que se presentó y todavía se presenta en la mayoría de los libros de texto. Pero, ya ves, no estaba tratando de programar en C ++, estaba tratando de encontrar la forma correcta de lidiar con el software. ...

Tuve muchos comienzos falsos. Por ejemplo, pasé años tratando de encontrar algún uso para la herencia y los virtuales, antes de entender por qué ese mecanismo era fundamentalmente defectuoso y no debía usarse. Estoy muy feliz de que nadie pudiera ver todos los pasos intermedios, la mayoría de ellos fueron muy tontos.

(É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):

El apoyo de Bjarne Stroustrup fue crucial. Bjarne realmente quería STL en el estándar y si Bjarne quiere algo, lo consigue. ... Incluso me obligó a hacer cambios en STL que nunca haría para nadie más ... él es la persona más decidida que conozco. Él hace las cosas. Le llevó un tiempo comprender de qué se trataba STL, pero cuando lo hizo, estaba preparado para seguir adelante. También contribuyó a STL defendiendo la opinión de que más de una forma de programación era válida, sin fin de flak y bombo durante más de una década, y persiguiendo una combinación de flexibilidad, eficiencia, sobrecarga y seguridad de tipos en plantillas que hicieron posible STL. Me gustaría decir con bastante claridad que Bjarne es el diseñador de lenguaje preeminente de mi generación.

Max Lybbert
fuente
2
Interesante entrevista. Estoy bastante seguro de que lo he leído antes hace algún tiempo, pero definitivamente valió la pena volver a leerlo. :)
jalf
3
Una de las entrevistas más interesantes sobre programación que he leído. A pesar de que me deja con sed de más detalles ...
Felixyz
Muchas de las quejas que hace sobre lenguajes como Java ("No se puede escribir un max () genérico en Java que tome dos argumentos de algún tipo y tenga un valor de retorno del mismo tipo") solo fueron relevantes para versiones muy tempranas del lenguaje, antes de que se agregaran los genéricos. Incluso desde el principio, se sabía que los genéricos finalmente se agregarían, aunque (una vez que se descubrió una sintaxis / semántica viable), por lo que sus críticas son en gran medida infundadas. Sí, se necesitan genéricos de alguna forma para preservar la seguridad de escritura en un lenguaje tipado estáticamente, pero no, eso no hace que OO sea inútil.
Algún tipo
1
@SomeGuy No son quejas sobre Java per se. Está hablando de la programación "estándar" OO de SmallTalk o, por ejemplo, Java ". La entrevista es de finales de los 90 (menciona que trabajó en SGI, que dejó en 2000 para trabajar en AT&T). Los genéricos solo se agregaron a Java en 2004 en la versión 1.5 y son una desviación del modelo OO "estándar".
melpomene
24

La respuesta se encuentra en esta entrevista con Stepanov, el autor de la STL:

Si. STL no está orientado a objetos. Creo que la orientación a objetos es casi tan engañosa como la Inteligencia Artificial. Todavía tengo que ver un código interesante que proviene de estas personas OO.

Apilado
fuente
Bonita joya; ¿Sabes de qué año es?
Kos
2
@Kos, de acuerdo con web.archive.org/web/20000607205939/http://www.stlport.org/… la primera versión de la página vinculada es del 7 de junio de 2001. La página en la parte inferior dice Copyright 2001- 2008
alfC
@Kos Stepanov menciona trabajar en SGI en la primera respuesta. Dejó la SGI en mayo de 2000, por lo que presumiblemente la entrevista es más antigua que eso.
melpomene
18

¿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 :)

AraK
fuente
12
¿Qué, Taj Mahal es pequeño y elegante, y su casa es del tamaño de una montaña y un completo desastre? ;)
jalf
Los conceptos ya no son parte de c ++ 0x. Algunos de los mensajes de error se pueden evitar usando static_assertquizás.
KitsuneYMG
GCC 4.6 ha mejorado los mensajes de error de plantilla, y creo que 4.7+ son aún mejores con él.
David Stone
Un concepto es esencialmente la "interfaz" que estaba solicitando el OP. La única diferencia es que la "herencia" de un Concepto es implícita (si una clase tiene todas las funciones miembro correctas, es automáticamente un subtipo del Concepto) en lugar de explícita (una clase Java debe declarar explícitamente que implementa una interfaz) . Sin embargo, los subtipos implícitos y explícitos son OO válidos, y algunos lenguajes OO tienen una herencia implícita que funciona igual que Concepts. Entonces, lo que se dice aquí es básicamente "OO apesta: use plantillas. Pero las plantillas tienen problemas, así que use Conceptos (que son OO)".
Algún tipo
11

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 función / clase de plantilla, y no por la clase del tipo usado con la plantilla, que es un poco anti-uso de OOP.

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.

puede decir la función ... espera un iterador directo solo mirando su definición, donde necesitaría mirar la implementación o la documentación para ...

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::copybasá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

En genéricos, un argumento debe ser de una clase derivada de una interfaz (el equivalente de C ++ a la interfaz es una clase abstracta) especificada en la definición del genérico. Eso significa que todos los tipos de argumentos genéricos deben caber en una jerarquía. Eso impone restricciones innecesarias en los diseños requiere una previsión irrazonable por parte de los desarrolladores. Por ejemplo, si escribe un genérico y yo defino una clase, las personas no pueden usar mi clase como argumento para su genérico a menos que yo conozca la interfaz que especificó y haya derivado mi clase de ella. Eso es rígido

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

Steve Jessop
fuente
En interfaces declaradas explícitamente, dos palabras: clases de tipo. (Que es lo que Stepanov quiere decir con "concepto".)
pyon
"Si no puede hacer válidas las expresiones válidas requeridas para un concepto STL, obtendrá un error de compilación no especificado cuando cree una plantilla con el tipo". -- eso es falso. Pasar algo a la stdbiblioteca que no coincide con un concepto suele estar "mal formado, no se requiere diagnóstico".
Yakk - Adam Nevraumont
Es cierto que estaba jugando rápido y suelto con el término "válido". Solo quise decir que si el compilador no puede compilar una de las expresiones requeridas, informará algo.
Steve Jessop
8

"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.

Ben Hughes
fuente
7

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.

Cuéntanos algo sobre tu interés a largo plazo en la programación genérica.

..... Luego me ofrecieron un trabajo en los Laboratorios Bell trabajando en el grupo C ++ en bibliotecas C ++. Me preguntaron si podía hacerlo en C ++. Por supuesto, no conocía C ++ y, por supuesto, dije que podía. Pero no pude hacerlo en C ++, porque en 1987 C ++ no tenía plantillas, que son esenciales para habilitar este estilo de programación. La herencia fue el único mecanismo para obtener el carácter genérico y no fue suficiente.

Incluso ahora, la herencia de C ++ no es de mucha utilidad para la programación genérica. Discutamos por qué. Muchas personas han intentado usar la herencia para implementar estructuras de datos y clases de contenedor. Como sabemos ahora, hubo pocos o ningún intento exitoso. La herencia de C ++, y el estilo de programación asociado con ella son dramáticamente limitados. Es imposible implementar un diseño que incluya algo tan trivial como la igualdad al usarlo. Si comienza con una clase base X en la raíz de su jerarquía y define un operador de igualdad virtual en esta clase que toma un argumento del tipo X, deduzca la clase Y de la clase X. ¿Cuál es la interfaz de la igualdad? Tiene igualdad que compara Y con X. Usando animales como ejemplo (OO, la gente ama a los animales), define mamífero y deriva jirafa de mamífero. Luego defina una función miembro compañero, donde el animal se empareja con el animal y devuelve un animal. Luego deriva la jirafa del animal y, por supuesto, tiene una función de compañero donde la jirafa se empareja con el animal y devuelve un animal. Definitivamente no es lo que quieres. Si bien el apareamiento puede no ser muy importante para los programadores de C ++, la igualdad sí lo es. No conozco un solo algoritmo donde no se use la igualdad de algún tipo.

yowkee
fuente
5

El problema básico con

void MyFunc(ForwardIterator *I);

¿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
1
Bueno, yo tampoco: 1. No intente obtenerlo, ya que estoy escribiendo código genérico. O, 2. Consígalo usando cualquier mecanismo de reflexión que C ++ ofrezca en estos días.
einpoklum
2

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.

Jerry Coffin
fuente
0

¿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:

void MyFunc(ForwardIterator<MyType>& i)

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").

Tanktalus
fuente
sólo puede utilizar los <,> y = operadores del tipo y no sabe lo que son (aunque esto podría no ser lo que quería decir)
lhahne
Dependiendo del contexto, puede que no tengan sentido o que funcionen bien. Es difícil saberlo sin saber más acerca de MyType, lo cual, presumiblemente, el usuario hace, y nosotros no.
Tanktalus
0

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

template<class Value,class T>
Value euler_fwd(size_t N,double t_0,double t_end,Value y_0,const T& func)
    {
    auto dt=(t_end-t_0)/N;
    for(size_t k=0;k<N;++k)
        {y_0+=func(t_0 + k*dt,y_0)*dt;}
    return y_0;
    }

Tenga en cuenta que esta función también funcionará si Valuees un vector de algún tipo ( no std :: vector, que debe llamarse std::dynamic_arraypara evitar confusiones)

Si funces pequeño, esta función ganará mucho con la alineación. Ejemplo de uso

auto result=euler_fwd(10000,0.0,1.0,1.0,[](double x,double y)
    {return y;});

En 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 funcy 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ícita

class OdeFunction
    {
    public:
        virtual double operator()(double t,double y) const=0;
    };

template
double euler_fwd(size_t N,double t_0,double t_end,double y_0,const OdeFunction& func);

Pero la instanciación anterior solo funciona double, ¿ por qué no escribir la interfaz como plantilla?

template<class Value=double>
class OdeFunction
    {
    public:
        virtual Value operator()(double t,const Value& y) const=0;
    };

y se especializan para algunos tipos de valores comunes:

template double euler_fwd(size_t N,double t_0,double t_end,double y_0,const OdeFunction<double>& func);

template vec4_t<double> euler_fwd(size_t N,double t_0,double t_end,vec4_t<double> y_0,const OdeFunction< vec4_t<double> >& func); // (Native AVX vector with four components)

template vec8_t<float> euler_fwd(size_t N,double t_0,double t_end,vec8_t<float> y_0,const OdeFunction< vec8_t<float> >& func); // (Native AVX vector with 8 components)

template Vector<double> euler_fwd(size_t N,double t_0,double t_end,Vector<double> y_0,const OdeFunction< Vector<double> >& func); // (A N-dimensional real vector, *not* `std::vector`, see above)

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.

user877329
fuente
-1

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 interfacepalabra 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.

Eugene Yokota
fuente
Acordó que las interfaces no son solo OO. Pero la capacidad del polimorfismo en el sistema de tipos es (fue en Simula en los años 60). Las interfaces de módulos existían en Modula-2 y Ada, pero creo que funcionaban en el sistema de tipos de manera diferente.
andygavin