son - tipo de excepciones en c++
¿Qué piensas acerca de lanzar una excepción por no encontrada en C++? (11)
Sé que la mayoría de la gente piensa que es una mala práctica, pero cuando intentas hacer que tu clase sea de interfaz pública, solo funciona con referencias, mantiene los indicadores dentro y solo cuando es necesario, creo que no hay forma de devolver algo diciendo que el valor que estás buscando no existe en el contenedor.
class list { public: value &get(type key); };
Pensemos que no quiere que se vean punteros peligrosos en la interfaz pública de la clase, ¿cómo se devuelve un no encontrado en este caso, lanzando una excepción?
¿Cuál es su enfoque para eso? ¿Devuelve un valor vacío y busca el estado vacío? De hecho, uso el enfoque de lanzar, pero introduzco un método de comprobación:
class list { public: bool exists(type key); value &get(type key); };
Entonces, cuando me olvido de verificar que el valor existe primero obtengo una excepción, eso es realmente una excepción .
¿Como lo harias?
¿Accesor?
La idea del "iterador" propuesta antes que yo es interesante, pero el verdadero punto de los iteradores es la navegación a través de un contenedor. No como un simple accesorio.
Estoy de acuerdo con paercebal , un iterador es para iterar . No me gusta la forma en que lo hace STL. Pero la idea de un accesorio parece más atractiva. Entonces, ¿qué necesitamos? Un contenedor como clase que se siente como un booleano para probar pero se comporta como el tipo de devolución original. Eso sería factible con los operadores de elenco.
template <T> class Accessor { public: Accessor(): _value(NULL) {} Accessor(T &value): _value(&value) {} operator T &() const { if (!_value) throw Exception("that is a problem and you made a mistake somewhere."); else return *_value; } operator bool () const { return _value != NULL; } private: T *_value; };
Ahora, ¿algún problema previsible? Un ejemplo de uso:
Accessor <type> value = list.get(key); if (value) { type &v = value; v.doSomething(); }
STL Iterators?
La idea del "iterador" propuesta antes que yo es interesante, pero el verdadero punto de los iteradores es la navegación a través de un contenedor. No como un simple accesorio.
Si su acceso es uno entre muchos, entonces los iteradores son el camino a seguir, porque podrá usarlos para moverse en el contenedor. Pero si su acceso es un getter simple, capaz de devolver el valor o el hecho de que no hay ningún valor , entonces su iterador es quizás solo un puntero glorificado ...
Lo que nos lleva a ...
¿Punteros inteligentes?
El objetivo de los punteros inteligentes es simplificar la propiedad del puntero. Con un puntero compartido, obtendrá un ressource (memoria) que se compartirá, a cambio de una sobrecarga (los punteros compartidos deben asignar un entero como contador de referencia ...).
Debe elegir: su valor ya está dentro de un puntero compartido y, a continuación, puede devolver este puntero compartido (o un puntero débil). O su valor está dentro de un puntero sin formato. Luego puede devolver el puntero de la fila. No desea devolver un puntero compartido si su recurso aún no está dentro de un puntero compartido: Un mundo de cosas divertidas sucederá cuando su puntero compartido se salga del alcance y borre su valor sin decirle ...
:-pag
¿Punteros?
Si su interfaz es clara sobre la propiedad de sus recursos, y el hecho de que el valor devuelto puede ser NULO, entonces puede devolver un puntero simple y sin formato. Si el usuario de tu código es lo suficientemente tonto, ignora el contrato de interfaz de tu objeto, o juega aritméticos o lo que sea con tu puntero, entonces él / ella será lo suficientemente tonto como para romper cualquier otra forma en que elijas devolver el valor, por lo que no te molestes con los desafiados mentalmente ...
Valor indefinido
A menos que su tipo de Valor realmente tenga algún tipo de valor "indefinido", y el usuario lo sepa, y acepte manejarlo, es una solución posible, similar a la solución de puntero o iterador.
Pero no agregue un valor "indefinido" a su clase de valor debido al problema que ha planteado: terminará elevando la guerra de "referencias vs. puntero" a otro nivel de locura. Los usuarios de código quieren que los objetos que les asignan estén bien o que no existan. Tener que probar cada dos líneas de código que este objeto sigue siendo válido es una molestia, y se complejizará inútilmente el código de usuario, por su culpa.
Excepciones
Las excepciones generalmente no son tan costosas como a algunas personas les gustaría que fueran. Pero para un acceso sencillo, el costo podría no ser trivial, si su acceso se usa con frecuencia.
Por ejemplo, STL std :: vector tiene dos accesos a su valor a través de un índice:
T & std::vector::operator[]( /* index */ )
y:
T & std::vector::at( /* index */ )
La diferencia es que el []
no tira . Por lo tanto, si accede fuera del alcance del vector, estará solo, probablemente arriesgando la corrupción de la memoria y un bloqueo tarde o temprano. Por lo tanto, debería asegurarse de verificar el código que lo usa.
Por otro lado, at
is throwing . Esto significa que si accede fuera del rango del vector, obtendrá una excepción limpia. Este método es mejor si desea delegar a otro código el procesamiento de un error.
Yo uso personalmente el []
cuando estoy accediendo a los valores dentro de un bucle, o algo similar. Uso at
cuando siento que una excepción es la buena manera de devolver el código actual (o el código de llamada) el hecho de que algo salió mal.
¿Y qué?
En tu caso, debes elegir:
Si realmente necesita un acceso rápido como un rayo, entonces el accesorio de lanzamiento podría ser un problema. Pero esto significa que ya usó un generador de perfiles en su código para determinar que esto es un cuello de botella, ¿no es así?
;-)
Si sabe que no tener un valor puede suceder a menudo, y / o desea que su cliente propague un posible puntero semántico nulo / inválido / al valor al que se accede, entonces devuelva un puntero (si su valor está dentro de un puntero simple) o un puntero débil / compartido (si su valor es propiedad de un puntero compartido).
Pero si cree que el cliente no propagará este valor "nulo", o que no deberían propagar un puntero NULL (o un puntero inteligente) en su código, utilice la referencia protegida por la excepción. Agregue un método "hasValue" que devuelva un booleano y agregue un lanzamiento si el usuario intenta obtener el valor, incluso si no hay ninguno.
Por último, considere el código que usará el usuario de su objeto:
// If you want your user to have this kind of code, then choose either
// pointer or smart pointer solution
void doSomething(MyClass & p_oMyClass)
{
MyValue * pValue = p_oMyClass.getValue() ;
if(pValue != NULL)
{
// Etc.
}
}
MyValue * doSomethingElseAndReturnValue(MyClass & p_oMyClass)
{
MyValue * pValue = p_oMyClass.getValue() ;
if(pValue != NULL)
{
// Etc.
}
return pValue ;
}
// ==========================================================
// If you want your user to have this kind of code, then choose the
// throwing reference solution
void doSomething(MyClass & p_oMyClass)
{
if(p_oMyClass.hasValue())
{
MyValue & oValue = p_oMyClass.getValue() ;
}
}
Entonces, si su problema principal es elegir entre los dos códigos de usuario anteriores, su problema no es sobre el rendimiento, sino la "ergonomía del código". Por lo tanto, la solución de excepción no debe dejarse de lado debido a posibles problemas de rendimiento.
:-)
¿Qué hay de devolver un shared_ptr como resultado? Esto puede ser nulo si el artículo no se encontró. Funciona como un puntero, pero se ocupará de liberar el objeto por usted.
@aradtke, dijiste.
Estoy de acuerdo con paercebal, un iterador es para iterar. No me gusta la forma en que lo hace STL. Pero la idea de un accesorio parece más atractiva. Entonces, ¿qué necesitamos? Un contenedor como clase que se siente como un booleano para probar pero se comporta como el tipo de devolución original. Eso sería factible con los operadores de elenco. [..] Ahora, ¿algún problema previsible?
Primero, NO QUIERES OPERADOR bool. Vea la expresión de Safe Bool para más información. Pero sobre tu pregunta ...
Aquí está el problema, los usuarios necesitan ahora explict cast en casos. Los proxies tipo puntero (como iteradores, ref-counted-ptrs y punteros crudos) tienen una sintaxis concisa de ''obtener''. Proporcionar un operador de conversión no es muy útil si las personas que llaman tienen que invocarlo con un código adicional.
Comenzando con su referencia como ejemplo, la forma más concisa de escribirlo:
// ''reference'' style, check before use
if (Accessor<type> value = list.get(key)) {
type &v = value;
v.doSomething();
}
// or
if (Accessor<type> value = list.get(key)) {
static_cast<type&>(value).doSomething();
}
Esto está bien, no me malinterpreten, pero es más detallado de lo que debe ser. ahora considere si sabemos , por alguna razón, que list.get tendrá éxito. Entonces:
// ''reference'' style, skip check
type &v = list.get(key);
v.doSomething();
// or
static_cast<type&>(list.get(key)).doSomething();
Ahora regresemos al comportamiento del iterador / puntero:
// ''pointer'' style, check before use
if (Accessor<type> value = list.get(key)) {
value->doSomething();
}
// ''pointer'' style, skip check
list.get(key)->doSomething();
Ambos son bastante buenos, pero la sintaxis del puntero / iterador es un poco más corta. Podría darle al estilo ''referencia'' una función miembro ''get ()'' ... pero eso ya es para lo que son el operador * () y el operador -> ().
El Accessor de estilo ''puntero'' ahora tiene operador ''bool'' no especificado, operador * y operador->.
Y adivinen qué ... el puntero sin formato cumple estos requisitos, por lo que para prototipos, list.get () devuelve T * en lugar de Accessor. Luego, cuando el diseño de la lista sea estable, puede regresar y escribir el Accesor, un tipo de Proxy similar a un puntero.
El STL trata esta situación mediante el uso de iteradores. Por ejemplo, la clase std :: map tiene una función similar:
iterator find( const key_type& key );
Si no se encuentra la clave, se devuelve ''end ()''. Es posible que desee utilizar este enfoque de iterador o utilizar algún tipo de envoltorio para su valor de retorno.
El problema con exists () es que terminarás buscando dos veces cosas que sí existen (primero comprueba si está ahí y luego vuelve a encontrarlo). Esto es ineficiente, particularmente si (como su nombre de "lista" sugiere) su contenedor es uno donde la búsqueda es O (n).
Claro, podrías hacer un almacenamiento en caché interno para evitar la doble búsqueda, pero luego tu implementación se volverá más desordenada, tu clase se volverá menos general (ya que has optimizado para un caso particular), y probablemente no sea a prueba de excepciones ni a hilos -seguro.
Interesante pregunta. En C ++ es un problema utilizar exclusivamente referencias, supongo. En Java, las referencias son más flexibles y pueden ser nulas. No puedo recordar si es C ++ legal forzar una referencia nula:
MyType *pObj = nullptr;
return *pObj
Pero considero esto peligroso. De nuevo en Java lanzaría una excepción ya que esto es común allí, pero rara vez veo excepciones usadas tan libremente en C ++. Si estuviera haciendo una API puclic para un componente C ++ reutilizable y tuviera que devolver una referencia, supongo que tomaría la ruta de excepción. Mi verdadera preferencia es hacer que la API devuelva un puntero; Considero que los punteros son una parte integral de C ++.
La respuesta correcta (según Alexandrescu) es:
Optional
y hacer cumplir
En primer lugar, utilice el Accesorio, pero de una manera más segura sin inventar la rueda:
boost::optional<X> get_X_if_possible();
Luego crea un ayudante de enforce
:
template <class T, class E>
T& enforce(boost::optional<T>& opt, E e = std::runtime_error("enforce failed"))
{
if(!opt)
{
throw e;
}
return *opt;
}
// and an overload for T const &
De esta forma, dependiendo de lo que pueda significar la ausencia del valor, puede verificar explícitamente:
if(boost::optional<X> maybe_x = get_X_if_possible())
{
X& x = *maybe_x;
// use x
}
else
{
oops("Hey, we got no x again!");
}
o implícitamente:
X& x = enforce(get_X_if_possible());
// use x
Usas la primera forma cuando te preocupa la eficiencia o cuando quieres manejar la falla justo donde ocurre. La segunda forma es para todos los demás casos.
lo que prefiero hacer en situaciones como esta es tener un "lanzamiento" y para aquellas circunstancias en las que el rendimiento o la falla es común tener una función "tryGet" en la línea de "bool tryGet (tipo clave, valor ** pp)" Whoose contrato es que si se devuelve verdadero, entonces * pp == un puntero válido a algún otro objeto * pp es nulo.
(Me doy cuenta de que esta no es siempre la respuesta correcta, y mi tono es un poco fuerte, pero debes considerar esta pregunta antes de decidir por otras alternativas más complejas):
Entonces, ¿qué hay de malo en devolver un puntero?
He visto esto muchas veces en SQL, donde la gente hará todo lo posible para no lidiar con columnas NULL, como si tuvieran una enfermedad contagiosa o algo así. En su lugar, ingeniosamente se les ocurre un valor artificial "en blanco" o "inexistente" como -1, 9999 o algo así como ''@ X-EMPTY-X @''.
Mi respuesta: el lenguaje ya tiene una construcción para "no estar ahí"; adelante, no tengas miedo de usarlo.
No use una excepción en tal caso. C ++ tiene una sobrecarga de rendimiento no trivial para tales excepciones, incluso si no se lanza una excepción , y además hace que el razonamiento sobre el código sea mucho más difícil (véase la excepción de seguridad).
La mejor práctica en C ++ es una de las dos formas siguientes. Ambos se utilizan en el STL:
- Como señaló Martin, devuelve un iterador. En realidad, su iterador puede ser un
typedef
para un simple puntero, no hay nada que lo contradiga; de hecho, ya que esto es consistente con el STL, incluso podría argumentar que de esta manera es superior a devolver una referencia. - Devuelve un
std::pair<bool, yourvalue>
. Esto hace que sea imposible modificar el valor, ya que se llama una copia delpair
que no funciona con los miembros de referencia.
/EDITAR:
Esta respuesta ha engendrado bastante controversia, visible a partir de los comentarios y no tan visible de los muchos votos negativos que obtuvo. He encontrado esto bastante sorprendente.
Esta respuesta nunca fue considerada como el último punto de referencia. La respuesta "correcta" ya había sido dada por Martin: las excepciones reflejan el comportamiento en este caso bastante mal. Es semánticamente más significativo usar algún otro mecanismo de señalización que excepciones.
Multa. Apoyo completamente esta vista. No es necesario mencionarlo una vez más. En cambio, quería dar una faceta adicional a las respuestas. Si bien los aumentos de velocidad menores nunca deben ser la primera razón para tomar decisiones, pueden proporcionar argumentos adicionales y en algunos (pocos) casos, incluso pueden ser cruciales.
En realidad, he mencionado dos facetas: seguridad de rendimiento y excepciones. Creo que este último es bastante indiscutible. Si bien es extremadamente difícil dar fuertes garantías de excepciones (la más fuerte, por supuesto, ser "nothrow"), creo que es esencial: cualquier código que garantice no arrojar excepciones hace que todo el programa sea más fácil de razonar. Muchos expertos en C ++ enfatizan esto (por ejemplo, Scott Meyers en el ítem 29 de "Effective C ++").
Acerca de la velocidad. Martin York ha señalado que esto ya no se aplica a los compiladores modernos. Estoy respetuosamente en desacuerdo. El lenguaje C ++ hace necesario que el entorno realice un seguimiento, en tiempo de ejecución, de rutas de código que pueden desenrollarse en el caso de una excepción. Ahora bien, esta sobrecarga no es realmente tan grande (y es bastante fácil verificar esto). "No trivial" en mi texto anterior puede haber sido demasiado fuerte.
Sin embargo, me parece importante establecer la distinción entre lenguajes como C ++ y muchos lenguajes modernos y "administrados" como C #. Este último no tiene sobrecarga adicional siempre que no se produzca ninguna excepción porque la información necesaria para desenrollar la pila se mantiene de todos modos. En general, defiende mi elección de palabras.