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

197

Cada contenedor estándar tiene un método beginy endpara devolver iteradores para ese contenedor. Funciones gratuitas Sin embargo, C ++ 11 aparentemente ha introducido llaman std::beginy std::endque exigen las beginy endmiembros de funciones. Entonces, en lugar de escribir

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

tu escribirias

auto i = std::begin(v);
auto e = std::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 finalización para un contenedor. Sin embargo, no entra en detalles sobre por qué querrías hacerlo. Mirando el código, te ahorra un solo 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 envases no estándar, pero nuevamente, no entró en detalles.

Entonces, la pregunta es, ¿qué hacen exactamente las versiones de funciones gratuitas std::beginy lo que std::endhacen más allá de llamar a sus versiones de funciones miembro correspondientes, y por qué querría usarlas?

Jonathan M Davis
fuente
29
Es un personaje menos, guarda esos puntos para tus hijos: xkcd.com/297
HostileFork dice que no confíes en SE
De alguna manera odiaría usarlos porque tendría que repetir std::todo el tiempo.
Michael Chourdakis

Respuestas:

162

¿Cómo se llama .begin()y .end()en un C-array?

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

Matthieu M.
fuente
77
@JonathanMDavis: puede tener las endmatrices declaradas estáticamente ( int foo[5]) utilizando trucos de programación de plantillas. Una vez que se ha convertido en un puntero, por supuesto, no tienes suerte.
Matthieu M.
33
template<typename T, size_t N> T* end(T (&a)[N]) { return a + N; }
Hugh
66
@JonathanMDavis: Como lo indicaron los demás, ciertamente es posible obtener beginy enden una matriz C siempre y cuando no lo haya convertido usted mismo en un puntero, @Huw lo explica. En cuanto a por qué querrías: imagina que refactorizaste el código que estaba usando una matriz para usar un vector (o viceversa, por cualquier razón). Si ha estado usando beginy end, y tal vez algún tipo de definición inteligente, el código de implementación no tendrá que cambiar en absoluto (excepto quizás algunos de los typedefs).
Karl Knechtel
31
@JonathanMDavis: Las matrices no son punteros. Y para todos: en aras de poner fin a esta confusión cada vez más prominente, deje de referirse a (algunos) punteros como "matrices decaídas". No existe tal terminología en el lenguaje, y realmente no tiene utilidad. Los punteros son punteros, las matrices son matrices. Las matrices se pueden convertir en un puntero a su primer elemento implícitamente, pero todavía es un puntero antiguo normal, sin distinción con los demás. Por supuesto, no puede obtener el "fin" de un puntero, caso cerrado.
GManNickG
55
Bueno, aparte de las matrices, hay una gran cantidad de API que exponen aspectos similares a los contenedores. Obviamente, no puede modificar una API de terceros, pero puede escribir fácilmente estas funciones independientes de inicio / finalización.
edA-qa mort-ora-y
35

Considere el caso cuando tiene una biblioteca que contiene clase:

class SpecialArray;

tiene 2 métodos:

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

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

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 SpecialArrayIteratores algo 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 iy se epuede usar legalmente para iterar y acceder a valores de SpecialArray

GreenScape
fuente
8
Esto no debe incluir las template<>líneas. Está declarando una nueva sobrecarga de funciones, no especializando una plantilla.
David Stone el
33

El uso de las funciones beginy endgratis agrega una capa de indirección. Por lo general, eso se hace para permitir más flexibilidad.

En este caso se me ocurren algunos usos.

El uso más obvio es para matrices en C (no punteros en C).

Otra es cuando se intenta utilizar un algoritmo estándar en un contenedor no conforme (es decir, al contenedor le falta un .begin()método). Suponiendo que no puede simplemente arreglar el contenedor, la siguiente mejor opción es sobrecargar la beginfunción. Herb sugiere que siempre use la beginfunción para promover la uniformidad y consistencia en su código. En lugar de tener que recordar qué contenedores admiten el método beginy cuáles necesitan funcionar begin.

Como un aparte, rev la siguiente C ++ debería copiar D's notación pseudo-miembro . Si a.foo(b,c,d)no está definido, en su lugar lo intenta foo(a,b,c,d). Es solo un poco de azúcar sintáctica para ayudarnos a los humanos pobres que prefieren el orden del sujeto al orden verbal.

código_deft
fuente
55
La notación de pseudo miembro se parece a los métodos de extensión C # /. Net . Sin embargo, son útiles para diversas situaciones, como todas las características, pueden ser propensas a 'abuso'.
Gareth Wilson el
55
La notación de pseudo miembro es una bendición para la codificación con Intellisense; golpeando "a". muestra verbos relevantes, liberando el poder del cerebro de memorizar listas y ayudando a descubrir funciones API relevantes puede ayudar a prevenir la duplicación de funciones, sin tener que forzar las funciones de los no miembros en clases.
Matt Curtis
Hay propuestas para introducir eso en C ++, que usan el término Sintaxis de llamada de función unificada (UFCS).
underscore_d
17

Para responder a su pregunta, las funciones gratuitas begin () y end () por defecto no hacen más que llamar a las funciones miembro .begin () y .end () del contenedor. Desde <iterator>, se incluye de forma automática cuando se utiliza cualquiera de los contenedores estándar como <vector>, <list>, etc., que se 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 su pregunta es por qué prefieren las funciones gratuitas si lo único que hacen es llamar a las funciones miembro de todos modos. Eso realmente depende de qué tipo de objeto vestá en su código de ejemplo. Si el tipo de v es un tipo de contenedor estándar, por ejemplo vector<T> v;, no importa si usa las funciones gratuitas o miembro, hacen lo mismo. Si su objeto ves más genérico, como en el siguiente código:

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

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

template <class T>
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 matrices T = C y cadenas C. Ahora escribiendo 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 su código compatibilidad hacia atrás con tipos de secuencia C y compatibilidad hacia adelante con tipos de secuencia no stl (¡y tipos stl futuros!), con bajo costo para otros desarrolladores.

Nate
fuente
Buenos ejemplos. Sin embargo, no tomaría un enumu otro tipo fundamental por referencia; serán más baratos de copiar que indirectos.
underscore_d
6

Una ventaja de std::beginy std::endes que sirven como puntos de extensión para implementar una interfaz estándar para clases externas.

Si desea utilizar la CustomContainerclase con función de bucle o plantilla basada en rango que espera .begin()y .end()métodos, obviamente tendría que implementar esos métodos.

Si la clase proporciona esos métodos, eso no es un problema. Cuando no es así, deberías modificarlo *.

Esto no siempre es factible, por ejemplo, cuando se utiliza una biblioteca externa, especialmente comercial y de fuente cerrada.

En tales situaciones, std::beginy std::endresulta útil, ya que uno puede proporcionar API de iterador sin modificar la clase en sí, sino más bien sobrecargar funciones libres.

Ejemplo: suponga que desea implementar una count_iffunción que tome un contenedor en lugar de un par de iteradores. Tal código podría verse así:

template<typename ContainerType, typename PredicateType>
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<PredicateType&&>(predicate));
}

Ahora, para cualquier clase que desee utilizar con esta costumbre count_if, solo tiene que agregar dos funciones libres, en lugar de modificar esas clases.

Ahora, C ++ tiene un mecanismo llamado Búsqueda dependiente de argumentos (ADL), que hace que este enfoque sea aún más flexible.

En resumen, ADL significa que cuando un compilador resuelve una función no calificada (es decir, una función sin espacio de nombres, como en beginlugar 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 lo estén some_lib::beginy some_lib::end , dado CustomContainerque some_lib::también lo está, el compilador usará esas sobrecargas count_if.

Esa es también la razón para tener using std::begin;y using std::end;entrar count_if. Esto nos permite usar no calificado beginy end, por lo tanto, permitir ADL y permitir que el compilador elija std::beginy std::endcuando no se encuentren otras alternativas.

Podemos comer la cookie y tener la cookie, es decir, tener una manera de proporcionar una implementación personalizada de begin/ endmientras el compilador puede recurrir a las estándar.

Algunas notas:

  • Por la misma razón, hay otras funciones similares: std::rbegin/ rend, std::sizey std::data.

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

  • Usar std::beginy amigos es una idea particularmente buena al escribir código de plantilla, porque esto hace que esas plantillas sean más genéricas. Para los que no son plantillas, también podría usar métodos, cuando corresponda.

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

joe_chip
fuente
Buena respuesta, particularmente explicando abiertamente ADL, en lugar de dejarlo a la imaginación como todos los demás, ¡incluso cuando lo mostraban en acción!
underscore_d
5

Mientras que las funciones que no son miembros 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 de contenedor no estándar existente, preferiría definir sobrecargas de las funciones libres, en lugar de alterar la definición de la clase existente. Por lo tanto, para los contenedores no estándar son muy útiles y siempre usar 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 ser ponderado correctamente y la abstracción tampoco es buena. Aunque el uso de las funciones gratuitas no es una gran abstracción, sin embargo, rompe la compatibilidad con el código C ++ 03, que a esta temprana edad de C ++ 11 podría ser un problema para usted.

Christian Rau
fuente
3
En C ++ 03, puede usar boost::begin()/ end(), por lo que no hay incompatibilidad real :)
Marc Mutz - mmutz
1
@ MarcMutz-mmutz Bueno, aumentar la dependencia no siempre es una opción (y es una exageración si se usa solo para begin/end). Por lo tanto, consideraría que también es una incompatibilidad con C ++ 03 puro. Pero como se dijo, es una incompatibilidad bastante pequeña (y cada vez más pequeña), ya que C ++ 11 (al menos begin/enden particular) está recibiendo cada vez más adopción, de todos modos.
Christian Rau
0

En última instancia, el beneficio está en el código que se generaliza de modo que sea independiente del contenedor. Puede operar en un std::vector, una matriz o un rango sin cambios en el código en sí.

Además, los contenedores, incluso los que no son de propiedad, se pueden adaptar para que también se puedan usar de manera independiente mediante código utilizando accesores basados ​​en rangos no miembros.

Ver aquí para más detalles.

Jonathan Mee
fuente