programar - dev c++
¿Por qué las secuencias de archivos estándar de C++ no siguen más de cerca las convenciones RAII? (5)
Aunque las otras respuestas son válidas y útiles, creo que la razón real es más simple.
El diseño de iostreams es mucho más antiguo que la mayoría de las bibliotecas estándar y es anterior al uso generalizado de excepciones. Sospecho que para ser compatible con el código existente, el uso de excepciones se hizo opcional, no el valor predeterminado por no abrir un archivo.
Además, su pregunta solo es realmente relevante para las secuencias de archivos, los otros tipos de secuencias estándar no tienen open()
funciones miembro open()
o close()
, por lo que sus constructores no lanzan si no se puede abrir un archivo :-)
Para los archivos, es posible que desee verificar que la llamada de close()
tenido éxito, de modo que sepa si los datos se escribieron en el disco, por lo que es una buena razón para no hacerlo en el destructor, porque en el momento en que el objeto se destruye es demasiado tarde para hacer algo útil con él y casi con seguridad no querrá lanzar una excepción desde el destructor. Por lo tanto, un fstreambuf
llamará close en su destructor, pero también puede hacerlo manualmente antes de destruirlo si lo desea.
En cualquier caso, no estoy de acuerdo en que no siga las convenciones de RAII ...
¿Por qué los diseñadores de bibliotecas eligieron su enfoque en lugar de abrirse solo en constructores que lanzan un fallo?
NB RAII no significa que no pueda tener un miembro separado open()
además de un constructor que adquiera recursos, o no puede limpiar el recurso antes de la destrucción, por ejemplo, unique_ptr
tiene un miembro reset()
.
Además, RAII no significa que debe arrojar un error, o un objeto no puede estar en un estado vacío, por ejemplo, unique_ptr
puede construirse con un puntero nulo o construido por defecto, y por lo tanto también puede apuntar a nada y, por lo tanto, en algunos casos Necesitas comprobarlo antes de desreferenciación.
Los flujos de archivos adquieren un recurso en la construcción y lo liberan en la destrucción, eso es RAII en lo que a mí respecta. Lo que está objetando es que se requiere una verificación, que huele a inicialización en dos etapas, y estoy de acuerdo en que es un poco maloliente. Aunque no lo hace RAII.
En el pasado, he resuelto el olor con una clase CheckedFstream
, que es un envoltorio simple que agrega una característica única: lanzar el cosntructor si no se puede abrir la secuencia. En C ++ 11 es tan simple como esto:
struct CheckedFstream : std::fstream
{
CheckedFstream() = default;
CheckedFstream(std::string const& path, std::ios::openmode m = std::ios::in|std::ios::out)
: fstream(path, m)
{ if (!is_open()) throw std::ios::failure("Could not open " + path); }
};
¿Por qué las secuencias de la biblioteca estándar de C ++ utilizan semántica open()
/ close()
desacoplada de la vida útil del objeto? El cierre de la destrucción aún podría técnicamente hacer que las clases sean RAII, pero la independencia de adquisición / liberación deja huecos en los ámbitos donde los manejadores no pueden apuntar a nada, pero aún necesitan controles de tiempo de ejecución para atraparlos.
¿Por qué los diseñadores de bibliotecas eligieron su enfoque en lugar de abrirse solo en constructores que lanzan un fallo?
void foo() {
std::ofstream ofs;
ofs << "Can''t do this!/n"; // XXX
ofs.open("foo.txt");
// Safe access requires explicit checking after open().
if (ofs) {
// Other calls still need checks but must be shielded by an initial one.
}
ofs.close();
ofs << "Whoops!/n"; // XXX
}
// This approach would seem better IMO:
void bar() {
std_raii::ofstream ofs("foo.txt"); // throw on failure and catch wherever
// do whatever, then close ofs on destruction ...
}
Una mejor redacción de la pregunta podría ser por qué vale la pena tener acceso a un fstream
no abierto. El control de la duración del archivo abierto a lo largo de la vida útil del manejador no me parece una carga, sino un beneficio de seguridad.
De esta manera obtienes más y nada menos.
Obtienes lo mismo : Aún puedes abrir el archivo a través del constructor. Aún obtienes RAII: cerrará automáticamente el archivo en la destrucción del objeto.
Obtendrá más : puede usar la misma transmisión para volver a abrir otro archivo; puede cerrar el archivo cuando lo desee, sin estar restringido a esperar a que el objeto salga del alcance o se destruya (esto es muy importante).
No obtienes nada menos : la ventaja que ves no es real. Usted dice que a su manera no tiene que verificar en cada operación. Esto es falso La transmisión puede fallar en cualquier momento, incluso si se abrió con éxito (el archivo).
En cuanto a la comprobación de errores frente a las excepciones de lanzamiento, consulte la respuesta de @PiotrS . Conceptualmente no veo ninguna diferencia entre tener que verificar el estado de retorno y tener que detectar un error. El error sigue ahí; La diferencia es cómo lo detectas. Pero como apunta por @PiotrS puedes optar por ambos.
Depende de lo que estés haciendo, leyendo o escribiendo. Puede encapsular un flujo de entrada de manera RAII, pero no es cierto para los flujos de salida. Si el destino es un archivo de disco o un socket de red, NUNCA, NUNCA ponga fclose / close en el destructor. Debido a que necesita verificar el valor de retorno de fclose, y no hay manera de reportar un error ocurrido en el destructor. ver ¿Cómo puedo manejar un destructor que falla
Las secuencias de archivos de la biblioteca estándar proporcionan RAII, en el sentido de que llamar al destructor en uno cerrará cualquier archivo que esté abierto. Sin embargo, al menos en el caso de la salida, esta es una medida de emergencia, que solo debe usarse si ha encontrado otro error y no va a utilizar el archivo que se estaba escribiendo de todos modos. (Una buena práctica de programación sería eliminarlo). Generalmente, debe verificar el estado de la transmisión después de haberla cerrado, y esta es una operación que puede fallar, por lo que no debe hacerse en el destructor.
Para la entrada, no es tan importante, ya que de todos modos habrá verificado el estado después de la última entrada, y la mayoría de las veces, habrá leído hasta que falla una entrada. Pero parece razonable tener la misma interfaz para ambos; Sin embargo, desde el punto de vista de la programación, normalmente puede dejar que el cierre en el destructor haga su trabajo en la entrada.
Con respecto a la open
: puede abrir el constructor con la misma facilidad, y para usos aislados como lo muestra, esta es probablemente la solución preferida. Pero hay casos en los que es posible que desee reutilizar un std::filebuf
, abrirlo y cerrarlo explícitamente, y por supuesto, en casi todos los casos, querrá manejar un error al abrir el archivo inmediatamente, en lugar de hacerlo por alguna excepción. .
Los diseñadores de la biblioteca te dieron la alternativa:
std::ifstream file{};
file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try
{
file.open(path); // now it will throw on failure
}
catch (const std::ifstream::failure& e)
{
}