Algoritmos STL: ¿Por qué no hay una interfaz adicional para los contenedores (adicional a los pares de iteradores)?

Me pregunto por qué el STL no sobrecarga sus funciones de algoritmo de manera que pueda llamarlas simplemente proporcionando un contenedor y sin tomar la forma más detallada de pasar los iteradores de inicio y finalización. Por supuesto, entiendo por qué también queremos usar un par de iteradores para procesar subsecuencias de un contenedor / matriz, sin embargo, casi todas las llamadas a estos métodos utilizan un contenedor completo:

std::for_each(myVector.begin(), myVector.end(), doSomething); 

Me parece más conveniente, legible y fácil de mantener que solo escribo

 std::for_each(myVector, doSomething); 

¿Hay alguna razón por la cual STL no proporciona estas sobrecargas ? [EDITAR: ¡No me refiero a reemplazar la interfaz con esta restringida, sino también a proporcionar una interfaz de contenedor!] ¿Introducen ambigüedad? Estoy pensando en algo como esto:

 template inline _Funct for_each(_Container c, _Funct f) { return for_each(begin(c), end(c), f); } 

¿Me estoy perdiendo de algo?

Introducen ambigüedad para muchos algoritmos. Se ve mucho

 template void do_something(iterator, iterator); template void do_something(iterator, iterator, funct); 

Si agrega sobrecargas adicionales

 template void do_something(container, funct); 

el comstackdor tendrá problemas para descifrar qué do_something(x, y) . Si y son del mismo type , coincidirá tanto con iterator = type como con container = type, funct = type . *)

C ++ 11 intentó resolver esto con “conceptos” que podrían reconocer la diferencia entre un contenedor y un iterador. Sin embargo, estos “conceptos” resultaron ser demasiado complicados para convertirlo en el estándar, por lo que tampoco estas sobrecargas.

*) los comstackdores no están de acuerdo, el comstackdor de Comeau afirma que es ambiguo, g ++ 4.5 y MSVC 10 llama a la primera función.


Después de una discusión extremadamente larga en los comentarios, aquí hay un ejemplo en el que no funciona como se esperaba, usando un adaptador de contenedor que también puede funcionar como un predicado.

 #include  #include  template void test(iterator, iterator) { std::cout << "test iterator\n"; } template void test(iterator, iterator, predicate) { std::cout << "test iterator, predicate\n"; } template void test(const container& cont, predicate compare) { std::cout << "test container, predicate\n"; test(cont.begin(), cont.end(), compare); } template class adapter { public: typedef typename container::iterator iterator; adapter(container* cont) : cont(cont) { } iterator begin() const { return cont->begin(); } iterator end() const { return cont->end(); } bool operator()(const iterator& one, const iterator& two) { return *one < *two; } private: container* cont; }; int main() { std::vector v; adapter> a(&v); test(a, a); } 

Salida:

iterador de prueba

http://ideone.com/wps2tZ

Desafortunadamente, este es un problema mucho más genérico; a saber, que los iteradores se diseñaron para vencer a esas pésimas API de C y soluciones de tipo “poner los algoritmos como métodos de cada contenedor individual” al estilo de Java. Son la solución genérica de primera generación y no sorprende que, reflexionando, no fueran tan buenas como otras posibles soluciones genéricas que se pueden obtener después de pasar veinte años pensando en ello.

Agregar estas sobrecargas de contenedor sería simplemente ayudando a una pequeña parte del espacio problemático; e incluso podría empeorar las cosas en el futuro. La solución es rangos, que C ++ busca introducir lo antes posible.

Para entender que creo que uno debe comprender la filosofía de los algoritmos de C ++. Hagamos esta pregunta primero:

¿Por qué los algoritmos de C ++ se implementan como funciones libres en lugar de funciones de miembros?

Bueno, la respuesta es bastante simple: evitar explosiones de implementación. Supongamos que tiene contenedores M y N algoritmos, y si los implementa como miembros de los contenedores, entonces habrá implementaciones M*N Hay dos problemas (relacionados) en este enfoque:

  • Primero, no hace uso de la reutilización de código. La mayoría de las implementaciones se repetirán.
  • En segundo lugar, las explosiones de implementación, que es una consecuencia directa de lo anterior.

C ++ resuelve estos problemas implementándolos como funciones libres, de modo que usted solo tiene implementaciones de N Cada uno de los algoritmos que opera en un contenedor toma un par de iteradores, que define el rango . Si desea sobrecargas que tengan contenedor, en lugar de par de iteradores, entonces el Estándar tiene que proporcionar dichas sobrecargas para cada uno de los algoritmos, y habrá implementaciones 2*N que prácticamente anulan el propósito por el que C ++ ha separado los algoritmos de los contenedores en primer lugar, y la mitad de estas funciones no hacen nada que la otra mitad no pueda hacer.

Entonces no creo que sea un problema. Para evitar un solo argumento, ¿por qué implementar N más funciones (que también imponen algunas restricciones sobre su uso, como que no se pueden pasar punteros a él)? Sin embargo, si los progtwigdores desean tales funciones en su utilidad, ¡pueden implementarlas en cualquier momento junto con muchas otras basadas en el algoritmo estándar!


Usted comentó:

Bueno, las implementaciones 2 * N son, de hecho, solo implementaciones N. Los otros N son sobrecargas en línea que llaman directamente a la versión “real” del algoritmo, por lo que son solo un encabezado. Proporcionar sobrecargas de contenedores no frustra el propósito de separar los algoritmos de los contenedores, ya que (como puede ver en mi ejemplo) pueden usar plantillas para manejar todo tipo de contenedores.

Basado en esta lógica, uno puede argumentar muy bien para los algoritmos M*N Así que conviértelos también en funciones miembro (y llama a las funciones gratuitas internamente). Estoy seguro de que muchos chicos de OOP preferirían

 auto result = container.accumulate(val); 

encima

 auto result = std::accumulate(container.begin(), container.end(), val); 

Aquí hay una respuesta relevante del blog de Herb Sutter: ¿Por qué no hay algoritmos basados ​​en contenedores ? Muestra contraejemplos tal como lo hizo Bo Persson en su respuesta anterior.

Hay una biblioteca de operadores de rango con la intención de arreglar eso. La verbosidad se cortó varias veces.

Tu ejemplo se vería así:

 auto newVector = myVector * doSomething; 

Sí, doSomething – está sin paréntesis.

Idioma familiar de shell (con algoritmo std):

 auto t = vector{3,2,1,4} | sort | unique; 

Debe señalarse que es muy fácil definir sus propias envolturas triviales para agregar versiones en contenedores.

Por ejemplo:

 template Func for_each(Container& c, Func f) { return std::for_each(c.begin(), c.end(), f); } 

Ahora puedes hacer la simple llamada que quieras. No hay ambigüedad porque tus contenedores no están en el espacio de nombres estándar. Puede definir sobrecargas que toman const Container &. Si quieres versiones que llamen a los métodos del intérprete de C ++ – 11 (por ejemplo, cbegin ()), creo que deberás nombrar el wrapper de forma diferente. Yo uso for_each_const.

Obviamente, como otros usuarios mencionaron, es un problema complejo, por lo que desafortunadamente ha pasado mucho tiempo y todavía no hay solución en la biblioteca estándar. Sin embargo, ya hay bibliotecas de rango disponibles como Boost :: Range y una en las bibliotecas de Adobe Source que proporcionan no solo la simplicidad de la interfaz que describes en tu pregunta, sino también algunas características más sofisticadas.

Su ejemplo funciona perfectamente con Boost (estamos using namespace boost::range::adaptors continuación):

boost::for_each(myVector, doSomething);

También podemos myVector rápida y sencilla:

boost::for_each(myVector | sliced(10, 20), doSomething)

Incluso podemos comprimir myVector con otro, filtrar por un predicado y muestrear todos los demás elementos de los pares resultantes en una sola statement simple (esto requiere que desempaque en doSomethingElse las tuplas producidas por boost::combined ):

boost::for_each( boost::combined(myVector, myOtherVector) | strided(2), doSomethingElse)