Implementar correctamente finalmente el bloque usando C++ lambda
c++11 finally (2)
Quiero implementar un bloque final en mi programa C ++, y el lenguaje ciertamente tiene las herramientas para hacerlo si no es una instalación nativa. Me preguntaba cuál es la mejor manera de hacer esto?
Esta simple implementación parece ser 100% segura.
template< typename t >
class sentry {
t o;
public:
sentry( t in_o ) : o( std::move( in_o ) ) {}
sentry( sentry && ) = delete;
sentry( sentry const & ) = delete;
~ sentry() noexcept {
static_assert( noexcept( o() ),
"Please check that the finally block cannot throw, "
"and mark the lambda as noexcept." );
o();
}
};
template< typename t >
sentry< t > finally( t o ) { return { std::move( o ) }; }
La noexcept
es importante porque no desea lanzar una excepción cuando su función ya está saliendo debido a una excepción. (Eso lleva a la terminación inmediata). C ++ no comprueba que la lambda realmente no puede lanzar nada; Usted lo verifica manualmente y lo marca sin noexcept
. (Vea abajo.)
La función de fábrica es necesaria porque, de lo contrario, no hay forma de obtener un tipo dependiente de un lambda.
Los constructores de copiar y mover deben eliminarse porque podrían usarse para generar implícitamente un objeto temporal, que implementaría otro centinela, que llamaría al bloque prematuramente en la destrucción. Pero el operador de asignación predeterminado se deja intacto porque si ya tiene dos centinelas que hacen cosas diferentes, está bien asignarlos. (Algo teórico, pero como sea.)
Sería bueno si el constructor fuera explicit
, pero eso parece excluir la inicialización in situ del valor de retorno. Dado que la clase no es móvil, el objeto que vive en el alcance de la persona que llama debe ser inicializado directamente por la expresión en la declaración de return
.
Para usar, solo define una guarda como esta:
auto && working_state_guard = finally( [&]() noexcept {
reset_working_state();
} );
Es esencial vincularse a una referencia, porque declarar un objeto real en el ámbito de la llamada requeriría moverlo inicializando ese objeto desde el valor de retorno de la función.
Alrededor de la versión 4.7, g++ -Wall
daría una advertencia de que el guardia no está en uso. Ya sea que esté codificando o no, puede agregar un poco de seguridad y documentación al final de la función con un idioma:
static_cast< void >( working_state_guard );
Esto le permite al lector conocer la ejecución del código desde el principio del alcance, y puede servir como un recordatorio para que vuelva a verificar cuando copie y pegue el código.
Uso.
int main() {
auto && guard = finally( []() noexcept {
try {
std::cout << "Goodbye!/n";
} catch ( ... ) {
// Throwing an exception from here would be worse than *anything*.
}
} );
std::cin.exceptions( std::ios::failbit );
try {
float age;
std::cout << "How old are you?/n";
std::cin >> age;
std::cout << "You are " << age << " years (or whatever) old/n";
} catch ( std::ios::failure & ) {
std::cout << "Sorry, didn''t understand that./n";
throw;
}
static_cast< void >( guard );
}
Esto produce salida como
$ ./sentry
How old are you?
3
You are 3 years (or whatever) old.
Goodbye!
$ ./sentry
How old are you?
four
Sorry, didn''t understand that.
Goodbye!
terminate called after throwing an instance of ''std::ios_base::failure''
what(): basic_ios::clear
Abort trap: 6
¿Cómo cancelar la acción de ser realizada?
Observando algunos de los "intentos anteriores", veo un método de transacción transaccional commit()
. No creo que pertenezca a la implementación del bloque ScopeGuard / finally. La implementación de un protocolo es responsabilidad del funtor contenido, por lo que la división correcta del trabajo sería encapsular una bandera booleana en ella, como capturar una bandera local bool
y voltear la bandera cuando la transacción está completa.
Del mismo modo, intentar cancelar la acción al reasignar el propio funtor es solo un enfoque desordenado. Generalmente se prefiere un caso adicional dentro del protocolo existente, a inventar un nuevo protocolo alrededor del antiguo.
Una solución alternativa utilizando std :: function. No se necesita función de fábrica. No hay instanciación de plantilla por uso (¿mejor huella ?!). No se requiere std :: move and && stuff, no se necesita auto;)
class finally
{
std::function<void()> m_finalizer;
finally() = delete;
public:
finally( const finally& other ) = delete;
finally( std::function<void()> finalizer )
: m_finalizer(finalizer)
{
}
~finally()
{
std::cout << "invoking finalizer code" << std::endl;
if( m_finalizer )
m_finalizer();
}
};
Uso:
int main( int argc, char * argv[] )
{
bool something = false;
try
{
try
{
std::cout << "starting" << std::endl;
finally final([&something]() { something = true; });
std::cout << "throwing" << std::endl;
throw std::runtime_error("boom");
}
catch(std::exception & ex )
{
std::cout << "inner catch" << std::endl;
throw;
}
}
catch( std::exception & ex )
{
std::cout << "outer catch" << std::endl;
if( something )
{
std::cout << "works!" << std::endl;
}
else
{
std::cout << "NOT working!" << std::endl;
}
}
std::cout << "exiting" << std::endl;
return 0;
}
Salida:
comenzando
lanzamiento
invocando código finalizador
captura interior
captura exterior
¡trabajos!
saliendo