vector :: at vs vector :: operador []

95

Sé que at()es más lento que []debido a su verificación de límites, que también se discute en preguntas similares como Vector C ++ a / [] velocidad del operador o :: std :: vector :: at () vs operador [] << ¡resultados sorprendentes! ¡5 a 10 veces más lento / más rápido! . Simplemente no entiendo para qué at()sirve el método.

Si tengo un vector simple como este: std::vector<int> v(10);y decido acceder a sus elementos usando en at()lugar de []en una situación cuando tengo un índice iy no estoy seguro si está en los límites de los vectores, me obliga a envolverlo con try-catch bloque :

try
{
    v.at(i) = 2;
}
catch (std::out_of_range& oor)
{
    ...
}

aunque puedo obtener el mismo comportamiento usando size()y verificando el índice por mi cuenta, lo que parece más fácil y muy conveniente para mí:

if (i < v.size())
    v[i] = 2;

Entonces mi pregunta es:
¿Cuáles son las ventajas de usar vector :: at sobre vector :: operator [] ?
¿Cuándo debería usar vector :: at en lugar de vector :: size + vector :: operator [] ?

LihO
fuente
11
+1 muy buena pregunta !! pero no creo que en () sea el que se usa comúnmente.
Rohit Vipin Mathews
10
Tenga en cuenta que en su código de ejemplo if (i < v.size()) v[i] = 2;, hay una posible ruta de código que no se asigna 2a ningún elemento de v. Si ese es el comportamiento correcto, genial. Pero a menudo no hay nada sensato que esta función pueda hacer cuando i >= v.size(). Por lo tanto, no hay ninguna razón en particular por la que no deba usar una excepción para indicar una situación inesperada. Muchas funciones simplemente se usan operator[]sin una verificación del tamaño, documento que idebe estar dentro del rango, y culpan a la persona que llama por el UB resultante.
Steve Jessop

Respuestas:

74

Yo diría que las excepciones que vector::at()arrojan realmente no están destinadas a ser detectadas por el código que las rodea inmediatamente. Son principalmente útiles para detectar errores en su código. Si necesita verificar los límites en tiempo de ejecución porque, por ejemplo, el índice proviene de la entrada del usuario, es mejor que tenga una ifdeclaración. Entonces, en resumen, diseñe su código con la intención de que vector::at()nunca genere una excepción, de modo que si lo hace, y su programa aborta, sea una señal de un error. (como un assert())

pmdj
fuente
1
+1 Me gusta la explicación de cómo separar el manejo de la entrada del usuario incorrecto (validación de entrada; se puede esperar una entrada no válida, por lo que no se considera algo excepcional) ... y errores en el código (el iterador de desreferenciación que está fuera de rango es excepcional cosa)
Bojan Komazec
Entonces dice que debería usar size()+ []cuando el índice depende de la entrada de los usuarios, usarlo asserten situaciones donde el índice nunca debe estar fuera de los límites para una fácil corrección de errores en el futuro y .at()en todas las demás situaciones (por si acaso, porque podría suceder algo incorrecto ... .)
LihO
8
@LihO: si su implementación ofrece una implementación de depuración de vectorentonces probablemente sea mejor usarla como la opción "por si acaso" en lugar de en at()todas partes. De esa manera, puede esperar un poco más de rendimiento en el modo de lanzamiento, en caso de que alguna vez lo necesite.
Steve Jessop
3
Sí, la mayoría de las implementaciones de STL en estos días admiten un modo de depuración que comprueba los límites incluso operator[], por ejemplo, gcc.gnu.org/onlinedocs/libstdc++/manual/… así que si su plataforma lo admite, ¡probablemente sea mejor que lo haga!
pmdj
1
@pmdj punto fantástico, que no conocía ... pero enlace huérfano. : P el actual es: gcc.gnu.org/onlinedocs/libstdc++/manual/debug_mode.html
underscore_d
16

me obliga a envolverlo con el bloque try-catch

No, no lo hace (el bloque try / catch puede estar en sentido ascendente). Es útil cuando desea que se lance una excepción en lugar de que su programa entre en un ámbito de comportamiento indefinido.

Estoy de acuerdo en que la mayoría de los accesos fuera de los límites a los vectores son un error del programador (en cuyo caso debería usarlo assertpara localizar esos errores más fácilmente; la mayoría de las versiones de depuración de las bibliotecas estándar lo hacen automáticamente). No desea utilizar excepciones que se puedan tragar en sentido ascendente para informar errores del programador: desea poder corregir el error .

Dado que es poco probable que un acceso fuera de los límites a un vector sea parte del flujo normal del programa (en el caso de que lo sea, tiene razón: verifique de antemano con en sizelugar de dejar que la excepción burbujee), estoy de acuerdo con su diagnóstico: ates esencialmente inútil.

Alexandre C.
fuente
Si no atrapo la out_of_rangeexcepción, entonces abort()se llama.
LihO
@LihO: No necesariamente ... el try..catchpuede estar presente en el método que llama a este método.
Naveen
12
Si nada más, ates útil en la medida en que de otra manera se encontraría escribiendo algo como if (i < v.size()) { v[i] = 2; } else { throw what_are_you_doing_you_muppet(); }. La gente suele pensar en las funciones de lanzamiento de excepciones en términos de "maldiciones, tengo que manejar la excepción", pero siempre que documente con cuidado lo que puede generar cada una de sus funciones, también se pueden usar como "genial, no tienes que comprobar una condición y lanzar una excepción ".
Steve Jessop
@SteveJessop: No me gusta lanzar excepciones para errores de programa, ya que pueden ser detectados por otros programadores. Las afirmaciones son mucho más útiles aquí.
Alexandre C.
6
@AlexandreC. bueno, la respuesta oficial a eso es que se out_of_rangederiva de logic_error, y otros programadores "deberían" saberlo mejor que capturar logic_errorlos mensajes de correo electrónico en sentido ascendente e ignorarlos. assertTambién se puede ignorar si tus colegas no quieren conocer sus errores, es más difícil porque tienen que compilar tu código NDEBUG;-) Cada mecanismo tiene sus méritos y defectos.
Steve Jessop
11

¿Cuáles son las ventajas de usar vector :: at sobre vector :: operator []? ¿Cuándo debería usar vector :: at en lugar de vector :: size + vector :: operator []?

El punto importante aquí es que las excepciones permiten la separación del flujo normal de código de la lógica de manejo de errores, y un solo bloque de captura puede manejar problemas generados desde cualquiera de los innumerables sitios de lanzamiento, incluso si se encuentran dispersos en lo profundo de las llamadas a funciones. Por lo tanto, no at()es necesariamente más fácil para un solo uso, sino que a veces se vuelve más fácil, y menos confusa de la lógica del caso normal, cuando tiene mucha indexación para validar.

También es digno de mención que en algunos tipos de código, un índice se incrementa de manera compleja y se usa continuamente para buscar una matriz. En tales casos, es mucho más fácil garantizar las comprobaciones correctas utilizando at().

Como ejemplo del mundo real, tengo un código que convierte C ++ en elementos léxicos, luego otro código que mueve un índice sobre el vector de tokens. Dependiendo de lo que encuentre, es posible que desee incrementar y verificar el siguiente elemento, como en:

if (token.at(i) == Token::Keyword_Enum)
{
    ASSERT_EQ(tokens.at(++i), Token::Idn);
    if (tokens.at(++i) == Left_Brace)
        ...
    or whatever

En este tipo de situación, es muy difícil comprobar si ha llegado de forma inapropiada al final de la entrada porque eso depende mucho de los tokens exactos encontrados. La verificación explícita en cada punto de uso es dolorosa, y hay mucho más margen para el error del programador a medida que entran en juego incrementos previos / posteriores, compensaciones en el punto de uso, razonamiento defectuoso sobre la validez continua de alguna prueba anterior, etc.

Tony Delroy
fuente
10

at puede ser más claro si tiene un puntero al vector:

return pVector->at(n);
return (*pVector)[n];
return pVector->operator[](n);

Dejando de lado el rendimiento, el primero de ellos es el código más simple y claro.

Brangdon
fuente
... especialmente cuando necesita un puntero al n -ésimo elemento de un vector.
Delfín
4

Primero, no se especifica si es at()o operator[]es más lento. Cuando no hay un error de límites, esperaría que tuvieran aproximadamente la misma velocidad, al menos en las compilaciones de depuración. La diferencia es que at()especifica exactamente lo que sucederá si hay un error de límites (una excepción), donde, como en el caso de operator[], es un comportamiento indefinido: un bloqueo en todos los sistemas que uso (g ++ y VC ++), al menos cuando se utilizan las banderas de depuración normales. (Otra diferencia es que una vez que estoy seguro de mi código, puedo obtener un aumento sustancial de velocidad paraoperator[] al desactivar la depuración. Si el rendimiento lo requiere, no lo haría a menos que fuera necesario).

En la práctica, at()rara vez es apropiado. Si el contexto es tal que sabe que el índice puede no ser válido, probablemente desee la prueba explícita (por ejemplo, que devuelva un valor predeterminado o algo), y si sabe que no puede ser inválido, desea abortar (y si no sabe si puede ser inválido o no, le sugiero que especifique la interfaz de su función con mayor precisión). Sin embargo, hay algunas excepciones en las que el índice no válido puede resultar del análisis de los datos del usuario, y el error debería provocar la interrupción de toda la solicitud (pero no hacer que el servidor caiga); en tales casos, una excepción es apropiada, yat() lo hará por usted.

James Kanze
fuente
4
¿Por qué esperaría que tuvieran aproximadamente la misma velocidad, cuando operator[]no está obligado a verificar los límites, mientras at()que sí? ¿Está implicando problemas de almacenamiento en caché, especulación y búfer de ramificación?
Sebastian Mach
@phresnel operator[]no está obligado a verificar los límites, pero todas las buenas implementaciones sí lo hacen. Al menos en modo de depuración. La única diferencia es lo que hacen si el índice está fuera de los límites: operator[]aborta con un mensaje de error, at()lanza una excepción.
James Kanze
2
Lo sentimos, se perdió su atributo "en modo de depuración". Sin embargo, no mediría la calidad del código en el modo de depuración. En el modo de liberación, la verificación solo la requiere at().
Sebastian Mach
1
@phresnel La mayor parte del código que he entregado ha estado en modo "depuración". Solo desactiva la verificación cuando los problemas de rendimiento realmente lo requieren. (Microsoft anterior a 2010 fue un problema aquí, ya std::stringque no siempre funcionaba si las opciones de verificación no se correspondían con las del tiempo de ejecución:, -MDy será mejor que desactive la verificación -MDd, y será mejor que tenga encendido.)
James Kanze
2
Soy más del campo que dice "código sancionado (garantizado) por norma"; por supuesto, puede realizar entregas en modo de depuración, pero cuando se realiza un desarrollo multiplataforma (incluido, pero no exclusivamente, el caso del mismo sistema operativo, pero diferentes versiones del compilador), confiar en el estándar es la mejor opción para las versiones y el modo de depuración se considera una herramienta para que el programador obtenga esa cosa en su mayoría correcta y robusta :)
Sebastian Mach
1

El objetivo de usar excepciones es que su código de manejo de errores puede estar más lejos.

En este caso específico, la entrada del usuario es un buen ejemplo. Imagine que desea analizar semánticamente una estructura de datos XML que utiliza índices para referirse a algún tipo de recurso que almacena internamente en un archivo std::vector. Ahora el árbol XML es un árbol, por lo que probablemente desee utilizar la recursividad para analizarlo. En el fondo, en la recursividad, puede haber una infracción de acceso por parte del escritor del archivo XML. En ese caso, normalmente querrá salir de todos los niveles de recursividad y simplemente rechazar todo el archivo (o cualquier tipo de estructura "más burda"). Aquí es donde resulta útil. Puede escribir el código de análisis como si el archivo fuera válido. El código de la biblioteca se encargará de la detección de errores y usted puede detectar el error en el nivel grueso.

Además, otros contenedores, como std::map, también tienen std::map::atque tiene una semántica ligeramente diferente que std::map::operator[]: at se puede usar en un mapa constante, mientras operator[]que no. Ahora, si quisieras escribir código independiente de contenedores, como algo que pudiera lidiar con const std::vector<T>&o const std::map<std::size_t, T>&, ContainerType::atsería tu arma preferida.

Sin embargo, todos estos casos suelen aparecer cuando se maneja algún tipo de entrada de datos no validada. Si está seguro de su rango válido, como debería estarlo normalmente operator[], normalmente puede usar , pero mejor aún, iteradores con begin()y end().

ltjax
fuente
1

Según este artículo, dejando de lado el rendimiento, no hay ninguna diferencia en usarat o operator[], solo si se garantiza que el acceso está dentro del tamaño del vector. De lo contrario, si el acceso se basa únicamente en la capacidad del vector, es más seguro utilizarlo at.

ahj
fuente
1
hay dragones. ¿Qué pasa si hacemos clic en ese enlace? (pista: ya lo sé, pero en StackOverflow preferimos los comentarios que no se pudren en el enlace, es decir, brindan un breve resumen sobre lo que quieres decir)
Sebastian Mach
Gracias por el consejo. Ahora está arreglado.
ahj
0

Nota: Parece que algunas personas nuevas están rechazando esta respuesta sin tener la cortesía de decir qué está mal. La respuesta a continuación es correcta y se puede verificar aquí .

En realidad, solo hay una diferencia: atcomprueba los límites mientras operator[]que no. Esto se aplica tanto a las versiones de depuración como a las versiones de lanzamiento y esto está muy bien especificado por los estándares. Es así de simple.

Esto lo convierte en atun método más lento, pero también es un mal consejo que no lo use at. Tienes que mirar números absolutos, no números relativos. Puedo apostar con seguridad que la mayor parte de su código está realizando operaciones más caras que at. Personalmente, trato de usar atporque no quiero que un error desagradable cree un comportamiento indefinido y se cuele en producción.

Shital Shah
fuente
1
Las excepciones en C ++ están destinadas a ser un mecanismo de manejo de errores, no una herramienta para depurar. Herb Sutter explica por qué lanzar std::out_of_rangeo cualquier forma de std::logic_errores, de hecho, un error lógico en sí mismo aquí .
Big Temp
@BigTemp: no estoy seguro de cómo se relaciona su comentario con esta pregunta y respuesta. Sí, las excepciones son un tema muy debatido, pero la pregunta aquí es la diferencia entre aty []y mi respuesta simplemente establece la diferencia. Yo personalmente uso el método "seguro" cuando el rendimiento no es un problema. Como dice Knuth, no realice una optimización prematura. Además, es bueno que los errores se detecten antes que en producción, independientemente de las diferencias filosóficas.
Shital Shah