¿Por qué todas las funciones de algoritmo toman solo rangos, no contenedores?

46

Hay muchas funciones útiles en <algorithm> , pero todas operan en "secuencias" - pares de iteradores. Por ejemplo, si tengo un contenedor y me gusta ejecutar std::accumulate en él, necesito escribir:

std::vector<int> myContainer = ...;
int sum = std::accumulate(myContainer.begin(), myContainer.end(), 0);

Cuando todo lo que pretendo hacer es:

int sum = std::accumulate(myContainer, 0);

Que es un poco más legible y más claro, a mis ojos.

Ahora puedo ver que puede haber casos en los que solo quieras operar en partes de un contenedor, por lo que es definitivamente útil tener la opción de rangos de aprobación. Pero al menos en mi experiencia, ese es un caso especial raro. Normalmente querré operar en contenedores enteros.

Es fácil escribir una función de contenedor que toma un contenedor y llama a begin() y end() en él, pero tales funciones de conveniencia no están incluidas en la biblioteca estándar.

Me gustaría saber el razonamiento detrás de esta elección de diseño de STL.

    
pregunta lethal-guitar 05.03.2014 - 14:14

5 respuestas

37
  

... es definitivamente útil tener la opción de pasar rangos. Pero al menos en mi experiencia, ese es un caso especial raro. Normalmente querré operar en contenedores enteros

Puede ser un caso especial raro en su experiencia , pero en realidad todo el contenedor es el caso especial, y el rango arbitrario Es el caso general.

Ya te has dado cuenta de que puedes implementar el caso contenedor completo utilizando la interfaz actual, pero no puedes hacer lo contrario.

Por lo tanto, el escritor de la biblioteca pudo elegir entre implementar dos interfaces por adelantado o solo una que aún cubra todos los casos.

  

Es fácil escribir una función de contenedor que toma un contenedor y las llamadas comienzan () y terminan () en él, pero tales funciones de conveniencia no están incluidas en la biblioteca estándar

Verdadero, especialmente porque ahora se incluyen las funciones gratuitas std::begin y std::end .

Entonces, digamos que la biblioteca proporciona la sobrecarga de conveniencia:

template <typename Container>
void sort(Container &c) {
  sort(begin(c), end(c));
}

ahora también debe proporcionar la sobrecarga equivalente tomando un funtor de comparación, y necesitamos proporcionar los equivalentes para todos los demás algoritmos.

Pero al menos cubrimos todos los casos en los que queremos operar en un contenedor lleno, ¿verdad? Bueno, no del todo. Considera

std::for_each(c.rbegin(), c.rend(), foo);

Si queremos manejar el funcionamiento de al revés en contenedores, necesitamos otro método (o par de métodos) por algoritmo existente.

Por lo tanto, el enfoque basado en el rango es más general en el sentido simple de que:

  • puede hacer todo lo que la versión de contenedor completo puede
  • el enfoque de todo el contenedor duplica o triplica el número de sobrecargas requeridas, mientras que sigue siendo menos poderoso
  • los algoritmos basados en rango también se pueden componer (puede apilar o encadenar adaptadores de iterador, aunque esto se hace más comúnmente en lenguajes funcionales y en Python)

Hay otra razón válida, por supuesto, que es ya mucho trabajo para estandarizar el STL, e inflarlo con envoltorios de conveniencia antes de que se haya utilizado ampliamente no sería un gran uso del tiempo limitado del comité. Si estás interesado, puedes encontrar Stepanov & El informe técnico de Lee aquí

Como se menciona en los comentarios, Boost.Range proporciona un enfoque más nuevo sin requerir cambios en el estándar.

    
respondido por el Useless 05.03.2014 - 15:44
18

Resulta que hay un artículo de Herb Sutter sobre este mismo tema . Básicamente, el problema es la ambigüedad de sobrecarga. Teniendo en cuenta lo siguiente:

template<typename Iter>
void sort( Iter, Iter ); // 1

template<typename Iter, typename Pred>
void sort( Iter, Iter, Pred ); // 2

Y añadiendo lo siguiente:

template<typename Container>
void sort( Container& ); // 3

template<typename Container, typename Pred>
void sort( Container&, Pred ); // 4

Será difícil distinguir correctamente 4 y 1 .

Los conceptos, tal como se propusieron pero finalmente no se incluyeron en C ++ 0x, lo habrían solucionado, y también es posible eludirlo utilizando enable_if . Para algunos de los algoritmos, no hay problema en absoluto. Pero decidieron no hacerlo.

Ahora, después de leer todos los comentarios y respuestas aquí, creo que range objects sería la mejor solución. Creo que echaré un vistazo a Boost.Range .

    
respondido por el lethal-guitar 06.03.2014 - 14:54
10

Básicamente una decisión legada. El concepto de iterador se basa en punteros, pero los contenedores no se modelan en matrices. Además, dado que las matrices son difíciles de pasar (en general, se necesita un parámetro de plantilla que no sea de tipo), a menudo una función solo tiene punteros disponibles.

Pero sí, en retrospectiva, la decisión es incorrecta. Habríamos estado mejor con un objeto de rango construible desde begin/end o begin/length ; ahora tenemos múltiples algoritmos de sufijo _n en su lugar.

    
respondido por el MSalters 05.03.2014 - 16:26
4

Al agregarlos no obtendrás ningún poder (ya puedes hacer todo el contenedor llamando a .begin() y .end() ), y agregaría una cosa más a la biblioteca que debe especificarse correctamente, agregada a la biblioteca. bibliotecas de los proveedores, probadas, mantenidas, etc. etc.

En resumen, probablemente no esté allí porque no vale la pena mantener un conjunto de plantillas adicionales solo para evitar que los usuarios de todo el contenedor escriban un parámetro de llamada a función adicional.

    
respondido por el Michael Kohne 05.03.2014 - 14:51
-1

Por ahora, enlace es una buena alternativa a std::for_each . Observe, no hay iteradores explícitos:

int a[5] = {1, 2, 3, 4, 5};
for (auto &i: a) { i *= 2; }

(Inspirado por enlace .)

    
respondido por el Camille Goudeseune 22.05.2015 - 21:54

Lea otras preguntas en las etiquetas