¿Por qué utilizar funciones de inicio y fin que no son miembros en C ++ 11?

Cada contenedor estándar tiene un método de begin y end para devolver iteradores para ese contenedor. Sin embargo, C ++ 11 aparentemente ha introducido funciones gratuitas llamadas std::begin y std::end que llaman a las funciones de miembro de begin y end . Entonces, en lugar de escribir

 auto i = v.begin(); auto e = v.end(); 

tu escribirías

 using std::begin; using std::end; auto i = begin(v); auto e = end(v); 

En su charla, Writing Modern C ++ , Herb Sutter dice que siempre debe usar las funciones gratuitas ahora cuando desee el iterador de inicio o final de un contenedor. Sin embargo, él no entra en detalles en cuanto a por qué querrías. Al mirar el código, le ahorra a todos un personaje. Entonces, en lo que respecta a los contenedores estándar, las funciones gratuitas parecen ser completamente inútiles. Herb Sutter indicó que había beneficios para los contenedores no estándar, pero una vez más, no entró en detalles.

Entonces, la pregunta es: ¿qué hacen exactamente las versiones de funciones libres de std::begin y std::end más allá de llamar a sus correspondientes versiones de funciones miembro, y por qué querrían usarlas?

¿Cómo llamas a .begin() y .end() en una matriz C?

Las funciones gratuitas permiten una progtwigción más genérica porque se pueden agregar después, en una estructura de datos que no puede modificar.

Considere el caso cuando tiene una biblioteca que contiene clase:

 class SpecialArray; 

tiene 2 métodos:

 int SpecialArray::arraySize(); int SpecialArray::valueAt(int); 

para iterar sobre sus valores que necesita heredar de esta clase y definir los métodos begin() y end() para casos en los que

 auto i = v.begin(); auto e = v.end(); 

Pero si siempre usas

 auto i = begin(v); auto e = end(v); 

Puedes hacerlo:

 template <> SpecialArrayIterator begin(SpecialArray & arr) { return SpecialArrayIterator(&arr, 0); } template <> SpecialArrayIterator end(SpecialArray & arr) { return SpecialArrayIterator(&arr, arr.arraySize()); } 

donde SpecialArrayIterator es algo así como:

 class SpecialArrayIterator { SpecialArrayIterator(SpecialArray * p, int i) :index(i), parray(p) { } SpecialArrayIterator operator ++(); SpecialArrayIterator operator --(); SpecialArrayIterator operator ++(int); SpecialArrayIterator operator --(int); int operator *() { return parray->valueAt(index); } bool operator ==(SpecialArray &); // etc private: SpecialArray *parray; int index; // etc }; 

ahora i y e pueden usarse legalmente para la iteración y el acceso de valores de SpecialArray

Usar las funciones gratuitas de begin y end agrega una capa de indirección. Usualmente eso se hace para permitir más flexibilidad.

En este caso, puedo pensar en algunos usos.

El uso más obvio es para C-arrays (no punteros c).

Otro es cuando se trata de usar un algoritmo estándar en un contenedor no conforme (es decir, al contenedor le falta un método .begin() ). Asumiendo que no puede simplemente arreglar el contenedor, la siguiente mejor opción es sobrecargar la función de begin . Herb sugiere que uses siempre la función de begin para promover la uniformidad y consistencia en tu código. En lugar de tener que recordar qué método de soporte de contenedores begin y qué función de necesidad begin .

Como un aparte, la próxima revisión de C ++ debe copiar la notación pseudo-miembro de D’s. Si a.foo(b,c,d) no está definido, en su lugar prueba foo(a,b,c,d) . Es solo un poco de azúcar sintáctico para ayudarnos a los humanos pobres que prefieren el orden de los sujetos y luego los verbos.

Para responder a su pregunta, las funciones gratuitas begin () y end () no hacen nada más que llamar a las funciones .begin () y .end () del miembro del contenedor. Desde , incluido automáticamente cuando utiliza cualquiera de los contenedores estándar como , , etc., obtiene:

 template< class C > auto begin( C& c ) -> decltype(c.begin()); template< class C > auto begin( const C& c ) -> decltype(c.begin()); 

La segunda parte de tu pregunta es por qué prefieren las funciones gratuitas si todo lo que hacen es llamar a las funciones miembro de todos modos. Eso realmente depende del tipo de objeto v en tu código de ejemplo. Si el tipo de v es un tipo de contenedor estándar, como vector v; entonces no importa si usa las funciones gratuitas o miembros, hacen lo mismo. Si su objeto v es más genérico, como en el siguiente código:

 template  void foo(T& v) { auto i = v.begin(); auto e = v.end(); for(; i != e; i++) { /* .. do something with i .. */ } } 

Luego, al usar las funciones de miembro, se rompe el código para las matrices T = C, las cadenas C, las enumeraciones, etc. Al utilizar las funciones que no son miembros, se anuncia una interfaz más genérica que las personas pueden ampliar fácilmente. Al usar la interfaz de función gratuita:

 template  void foo(T& v) { auto i = begin(v); auto e = end(v); for(; i != e; i++) { /* .. do something with i .. */ } } 

El código ahora funciona con arreglos T = C y cadenas C. Ahora escribe una pequeña cantidad de código de adaptador:

 enum class color { RED, GREEN, BLUE }; static color colors[] = { color::RED, color::GREEN, color::BLUE }; color* begin(const color& c) { return begin(colors); } color* end(const color& c) { return end(colors); } 

También podemos hacer que su código sea compatible con enumeraciones iterables. Creo que el punto principal de Herb es que usar las funciones gratuitas es tan fácil como usar las funciones miembro, y le da a tu código compatibilidad hacia atrás con los tipos de secuencia C y hacia adelante la compatibilidad con tipos de secuencia no stl (¡y tipos stl futuros!), con bajo costo para otros desarrolladores.

Mientras que las funciones de no miembro no proporcionan ningún beneficio para los contenedores estándar, su uso impone un estilo más consistente y flexible. Si en algún momento desea extender una clase contenedora no estándar existente, preferiría definir sobrecargas de las funciones gratuitas, en lugar de alterar la definición de la clase existente. Por lo tanto, para los contenedores que no son estándar, son muy útiles y usar siempre las funciones gratuitas hace que su código sea más flexible ya que puede sustituir el contenedor estándar por un contenedor no estándar más fácilmente y el tipo de contenedor subyacente es más transparente para su código. admite una variedad mucho más amplia de implementaciones de contenedores.

Pero, por supuesto, esto siempre tiene que ponderarse adecuadamente y la abstracción tampoco es buena. Aunque el uso de las funciones gratuitas no es tanto una sobre-abstracción, no obstante rompe la compatibilidad con el código C ++ 03, que a esta temprana edad de C ++ 11 podría ser un problema para usted.

Un beneficio de std::begin y std::end es que sirven como puntos de extensión para implementar una interfaz estándar para clases externas.

Si desea utilizar la clase CustomContainer con función de bucle o plantilla basada en .begin() que espera los .begin() y .end() , obviamente deberá implementar esos métodos.

Si la clase proporciona esos métodos, eso no es un problema. Cuando no lo haga, tendrá que modificarlo *.

Esto no siempre es factible, por ejemplo, al usar una biblioteca externa, especialmente comercial y de código cerrado.

En tales situaciones, std::begin y std::end son útiles, ya que uno puede proporcionar la API de iterador sin modificar la clase en sí, sino sobrecargar funciones gratuitas.

Ejemplo: supongamos que desea implementar la función count_if que toma un contenedor en lugar de un par de iteradores. Tal código podría verse así:

 template std::size_t count_if(const ContainerType& container, PredicateType&& predicate) { using std::begin; using std::end; return std::count_if(begin(container), end(container), std::forward(predicate)); } 

Ahora, para cualquier clase que quieras usar con este count_if personalizado, solo tienes que agregar dos funciones gratuitas, en lugar de modificar esas clases.

Ahora, C ++ tiene un mecanismo llamado Argument Dependent Lookup (ADL), que hace que este enfoque sea aún más flexible.

En resumen, ADL significa que cuando un comstackdor resuelve una función no calificada (es decir, función sin espacio de nombres, como begin lugar de std::begin ), también considerará las funciones declaradas en los espacios de nombres de sus argumentos. Por ejemplo:

 namesapce some_lib { // let's assume that CustomContainer stores elements sequentially, // and has data() and size() methods, but not begin() and end() methods: class CustomContainer { ... }; } namespace some_lib { const Element* begin(const CustomContainer& c) { return c.data(); } const Element* end(const CustomContainer& c) { return c.data() + c.size(); } } // somewhere else: CustomContainer c; std::size_t n = count_if(c, somePredicate); 

En este caso, no importa que los nombres calificados sean some_lib::begin y some_lib::end , ya que CustomContainer está en some_lib:: too, el comstackdor usará esas sobrecargas en count_if .

Esa es también la razón para using std::begin; y using std::end; en count_if . Esto nos permite utilizar un begin y end no calificados, por lo tanto, permite ADL y permite al comstackdor seleccionar std::begin y std::end cuando no se encuentran otras alternativas.

Podemos comer la cookie y tener la cookie, es decir, tener una forma de proporcionar una implementación personalizada de begin / end mientras que el comstackdor puede recurrir a las estándar.

Algunas notas:

  • Por la misma razón, existen otras funciones similares: std::rbegin / rend , std::size y std::data .

  • Como otras respuestas mencionan, std:: versiones tienen sobrecargas para matrices desnudas. Eso es útil, pero es simplemente un caso especial de lo que he descrito anteriormente.

  • Usar std::begin y friends es una buena idea cuando se escribe un código de plantilla, porque esto hace que esas plantillas sean más genéricas. Para los que no usan plantillas, es muy posible que también utilicen métodos, cuando corresponda.

PD, soy consciente de que esta publicación tiene casi 7 años. Lo encontré porque quería responder a una pregunta marcada como duplicada y descubrí que ninguna respuesta aquí menciona ADL.