Invocación inmediata de lambdas (técnica IIFE)

Introducción


En este post describiremos la técnica para realizar inicializaciones locales complejas recomendada por los C++ Core Guidelines. Hablamos aquí de aquellas inicializaciones que involucran típicamente la creación de objetos auxiliares y/o el empleo de sentencias de control. Nos referiremos a dicho procedimiento como IIFE (Immediately-invoked function expression) por su semejanza con la solución idiomática del mismo nombre utilizada en JavaScript.

En el contexto de C++, algunas fuentes prefieren el término IILE (Immediately-invoked lambda expression), dado que la técnica descansa en el empleo de expresiones lambda. Como comprobaremos a continuación, este procedimiento es particularmente útil a la hora de inicializar objetos que deban ser declarados constantes. Para más información, puedes consultar: http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-lambda-init


Cómo funciona


Consideremos nuevamente el ejemplo 5 del tercer artículo de nuestra serie de introducción a la biblioteca Range-v3. En éste, utilizábamos un fichero JSON Lines data.jsonl en el que cada una de sus líneas proporcionaba n parejas clave-valor ("dat_i" : value_i) con los que construir un agregado de datos Entry:

   struct Entry {       Type_1 dat_1;       // ...       Type_n dat_n;    };

Supongamos que deseamos almacenar la información del fichero en un vector estándar entries de objetos del tipo anterior. Asumamos, además, que una vez finalizado el trabajo de construcción, el contenedor debe permanecer inmutable y que, por tanto, debe declararse como constante. Sería muy conveniente lograr dicha inicialización sin involucrar a una función no-local que, de definirse, probablemente sólo fuese utilizada una vez. La solución ofrecida por la técnica IILE tomaría la forma siguiente:

   auto const entries = /* IILE */ []{  // (A)       auto res = std::vector<Entry>{};  // (B)       auto ifs = std::ifstream{"data.jsonl", std::ios::binary};       if (!ifs)          throw std::ios::failure{"unable to open the file"};       for (std::string const& line : ranges::getlines(ifs)) {          auto const j = nlohmann::json::parse(line);          res.push_back({             .dat_1  = j.at("dat_1"),             // ...             .dat_n  = j.at("dat_n")          });       }       return res;  // (C)    }();  // (D)

Distinguimos aquí:
  • La codificación de una expresión lambda que realiza el trabajo de inicialización (A).
  • La invocación automática de la función con el fin de asignar el valor calculado a nuestra variable (D).
En este caso, el tipo de la variable a generar, std::vector<Entry>, puede identificarse con facilidad a partir de la primera línea de código de la expresión lambda (B). Tras el cálculo de la variable, ésta es retornada en la última línea de código (C). La conocida optimización NRVO de nuestro compilador debiera eludir la copia y/o el movimiento del vector así producido. De forma notable, el contenedor entries inicializado de esta forma puede etiquetarse como constante.

Por supuesto, si considerásemos conveniente indicar más claramente el tipo retornado por la expresión lambda, podríamos modificar su signatura en la forma:

   auto const entries = /* IILE */ []()-> std::vector<Entry> {       // ...    }();


Acerca de std::invoke


Algunas referencias y foros de debate recomiendan una forma más explícita de realizar la llamada a función a través del uso de la función estándar std::invoke (C++17) del fichero de cabecera <functional>:

   auto const entries = std::invoke([]{       auto res = std::vector<Entry>{};  // ...       return res;    });

Se trata así de poner de manifiesto que nuestra lambda va a ser invocada de forma inmediata, sin necesidad de que el programador deba acudir al final de la expresión para comprobar si el código corresponde, en efecto, a una llamada a función (paréntesis () al final de la expresión) o, por el contrario, a una mera declaración (ausencia de paréntesis). Tengamos en cuenta, sin embargo, que la definición de la función invoke descansa en técnicas nada triviales de metaprogramación con templates, por lo que un uso excesivo de la misma podría ralentizar la compilación de nuestro código. En efecto, muchos programadores considerarían la construcción anterior como un abuso de la funcionalidad de invoke, destinada en origen al ámbito de la programación genérica.

En la práctica, bastaría comentar convenientemente el inicio de la expresión lambda para explicitar la inmediata invocación de la misma, tal y como hicimos en la línea (A) de nuestro primer código de ejemplo.

No hay comentarios:

Publicar un comentario