c++ - manager - ¿Deben los contenedores personalizados tener funciones gratuitas de inicio/finalización?
instalar google tag manager (1)
Al crear una clase de contenedor personalizado que se rige por las reglas habituales (es decir, funciona con algoritmos STL, funciona con código genérico de buen comportamiento, etc.), en C ++ 03 fue suficiente implementar soporte de iterador y funciones de inicio / finalización de miembro.
C ++ 11 presenta dos nuevos conceptos: basado en rango para loop y std :: begin / end. Range-based for loop entiende las funciones de inicio / finalización de los miembros, por lo que cualquier contenedor C ++ 03 es compatible con el rango de forma inmediata. Para los algoritmos, la forma recomendada (de acuerdo con "Escribir código C ++ moderno" de Herb Sutter) es usar std :: begin en lugar de función de miembro.
Sin embargo, en este punto tengo que preguntar: ¿es la forma recomendada de llamar a una función totalmente calificada de begin () (es decir, std :: begin (c)) o confiar en ADL y llamar a begin (c)?
La ADL parece inútil en este caso particular, ya que std :: begin (c) delega en c.begin () si es posible, los beneficios de ADL habituales no parecen aplicarse. Y si todo el mundo comienza a confiar en ADL, todos los contenedores personalizados deben implementar funciones gratuitas de inicio () / end () adicionales en sus espacios de nombres requeridos. Sin embargo, varias fuentes parecen implicar que las llamadas no calificadas para comenzar / finalizar son la forma recomendada (es decir, https://svn.boost.org/trac/boost/ticket/6357 ).
Entonces, ¿cuál es el camino de C ++ 11? ¿Deberían los autores de bibliotecas de contenedores escribir funciones adicionales de inicio / finalización para sus clases para admitir llamadas de inicio / finalización no calificadas en ausencia de utilizar el espacio de nombres estándar? o usando std :: begin ;?
Hay varios enfoques, cada uno con sus propios pros y contras. Por debajo de tres enfoques con un análisis de costo-beneficio.
ADL a través de un no miembro personalizado begin()
/ end()
La primera alternativa proporciona plantillas de funciones non-member begin()
y end()
dentro de un espacio de nombres legacy
para actualizar la funcionalidad requerida en cualquier clase o plantilla de clase que pueda proporcionarla, pero tiene, por ejemplo, las convenciones de nomenclatura incorrectas. El código de llamada puede entonces confiar en ADL para encontrar estas nuevas funciones. Código de ejemplo (basado en los comentarios de @Xeo):
// LegacyContainerBeginEnd.h
namespace legacy {
// retro-fitting begin() / end() interface on legacy
// Container class template with incompatible names
template<class C>
auto begin(Container& c) -> decltype(c.legacy_begin())
{
return c.legacy_begin();
}
// similarly for begin() taking const&, cbegin(), end(), cend(), etc.
} // namespace legacy
// print.h
template<class C>
void print(C const& c)
{
// bring into scope to fall back on for types without their own namespace non-member begin()/end()
using std::begin;
using std::end;
// works for Standard Containers, C-style arrays and legacy Containers
std::copy(begin(c), end(c), std::ostream_iterator<decltype(*begin(c))>(std::cout, " ")); std::cout << "/n";
// alternative: also works for Standard Containers, C-style arrays and legacy Containers
for (auto elem: c) std::cout << elem << " "; std::cout << "/n";
}
Pros : convención de llamadas coherentes y consistentes que funciona de manera completamente genérica
- funciona para cualquier contenedor estándar y tipos de usuario que definan miembros
.begin()
y.end()
- funciona para arreglos de estilo C
- se puede actualizar para que funcione (¡también para bucles range-for !) para cualquier plantilla de clase
legacy::Container<T>
que no tenga miembro.begin()
yend()
sin requerir modificaciones en el código fuente
Contras : requiere usar declaraciones en muchos lugares
-
std::begin
ystd::end
deben haberse incluido en todos los ámbitos explícitos de las llamadas como opciones de repliegue para las matrices de estilo C (posibles inconvenientes para los encabezados de las plantillas y molestias generales)
ADL a través de no miembro personalizado adl_begin()
y adl_end()
Una segunda alternativa es encapsular las declaraciones de uso de la solución anterior en un espacio de nombres de adl
separado al proporcionar plantillas de funciones no miembro adl_begin()
y adl_end()
, que también se pueden encontrar a través de ADL. Código de ejemplo (basado en los comentarios de @Yakk):
// LegacyContainerBeginEnd.h
// as before...
// ADLBeginEnd.h
namespace adl {
using std::begin; // <-- here, because otherwise decltype() will not find it
template<class C>
auto adl_begin(C && c) -> decltype(begin(std::forward<C>(c)))
{
// using std::begin; // in C++14 this might work because decltype() is no longer needed
return begin(std::forward<C>(c)); // try to find non-member, fall back on std::
}
// similary for cbegin(), end(), cend(), etc.
} // namespace adl
using adl::adl_begin; // will be visible in any compilation unit that includes this header
// print.h
# include "ADLBeginEnd.h" // brings adl_begin() and adl_end() into scope
template<class C>
void print(C const& c)
{
// works for Standard Containers, C-style arrays and legacy Containers
std::copy(adl_begin(c), adl_end(c), std::ostream_iterator<decltype(*adl_begin(c))>(std::cout, " ")); std::cout << "/n";
// alternative: also works for Standard Containers, C-style arrays and legacy Containers
// does not need adl_begin() / adl_end(), but continues to work
for (auto elem: c) std::cout << elem << " "; std::cout << "/n";
}
Pros : convención de llamadas coherente que funciona de manera completamente genérica
- las mismas ventajas que para la sugerencia de @ Xeo +
- las repetidas declaraciones de uso han sido encapsuladas (DRY)
Contras : un poco detallado
-
adl_begin()
/adl_end()
no es tan escueto comobegin()
/end()
- quizás tampoco sea tan idiomático (aunque es explícito)
- la deducción pendiente del tipo de retorno C ++ 14 también contaminará el espacio de nombres con
std::begin
/std::end
NOTA : No estoy seguro de si esto realmente mejora con el enfoque anterior.
Calificación explícitamente std::begin()
o std::end()
todas partes
Una vez que la verbosidad de begin()
/ end()
se ha abandonado de todos modos, ¿por qué no volver a las llamadas calificadas de std::begin()
/ std::end()
? Código de ejemplo:
// LegacyIntContainerBeginEnd.h
namespace std {
// retro-fitting begin() / end() interface on legacy IntContainer class
// with incompatible names
template<>
auto begin(legacy::IntContainer& c) -> decltype(c.legacy_begin())
{
return c.legacy_begin();
}
// similary for begin() taking const&, cbegin(), end(), cend(), etc.
} // namespace std
// LegacyContainer.h
namespace legacy {
template<class T>
class Container
{
public:
// YES, DOCUMENT REALLY WELL THAT THE EXISTING CODE IS BEING MODIFIED
auto begin() -> decltype(legacy_begin()) { return legacy_begin(); }
auto end() -> decltype(legacy_end()) { return legacy_end(); }
// rest of existing interface
};
} // namespace legacy
// print.h
template<class C>
void print(C const& c)
{
// works for Standard Containers, C-style arrays as well as
// legacy::IntContainer and legacy::Container<T>
std::copy(std::begin(c), std::end(c), std::ostream_iterator<decltype(*std::begin(c))>(std::cout, " ")); std::cout << "/n";
// alternative: also works for Standard Containers, C-style arrays and
// legacy::IntContainer and legacy::Container<T>
for (auto elem: c) std::cout << elem << " "; std::cout << "/n";
}
Pros : convención de llamadas coherente que funciona de forma casi genérica
- funciona para cualquier contenedor estándar y tipos de usuario que definan miembros
.begin()
y.end()
- funciona para arreglos de estilo C
Contras : un poco detallado y la adaptación no es genérica y un problema de mantenimiento
-
std::begin()
/std::end()
es un poco más detallado quebegin()
/end()
- solo se puede adaptar para que funcione (¡también para bucles range-for !) para cualquier clase
LegacyContainer
que no tenga miembros.begin()
yend()
(y para los cuales no haya código fuente!) proporcionando especializaciones explícitas de los no plantillas de función de miembrosbegin()
yend()
ennamespace std
- solo se puede
LegacyContainer<T>
en las plantillas de claseLegacyContainer<T>
añadiendo directamente las funciones de miembrobegin()
/end()
dentro del código fuente deLegacyContainer<T>
(que para las plantillas está disponible). El truconamespace std
no funciona aquí porque las plantillas de funciones no pueden ser parcialmente especializadas.
Qué usar?
El enfoque ADL a través de non-member begin()
/ end()
en el espacio de nombres propio de un contenedor es el enfoque idiomático C ++ 11, especialmente para funciones genéricas que requieren la actualización en clases heredadas y plantillas de clases. Es la misma expresión idiomática que para las funciones swap()
non-member que proporcionan los usuarios.
Para el código que solo utiliza contenedores estándar o matrices de estilo C, std::begin()
y std::end()
se pueden llamar a todas partes sin introducir declaraciones de uso, a expensas de llamadas más detalladas. Este enfoque puede incluso actualizarse, pero requiere tocar el namespace std
(para los tipos de clase) o las modificaciones de fuente en el lugar (para las plantillas de clase). Se puede hacer, pero no vale la pena el problema de mantenimiento.
En el código no genérico, donde el contenedor en cuestión es conocido en el momento de la codificación, uno podría incluso confiar en ADL para Contenedores Estándar solamente, y explícitamente calificar std::begin
/ std::end
para arreglos de estilo C. Pierde cierta consistencia de llamada, pero ahorra en el uso de declaraciones.