Funciones modernas de C ++ que todos los programadores deben conocer

El autor del material, cuya traducción publicamos hoy, dice que C ++, en su forma moderna, en comparación con lo que era este lenguaje hace varios años, ha cambiado significativamente para mejor. Por supuesto, estos cambios no ocurrieron de inmediato. Por ejemplo, en los viejos tiempos, C ++ carecía de dinamismo. No fue fácil encontrar una persona que pudiera decir que tiene sentimientos tiernos por este idioma. Todo cambió cuando los responsables de estandarizar el lenguaje decidieron dar paso a las innovaciones. En 2011, C ++ se convirtió en un lenguaje dinámico, un lenguaje que evoluciona constantemente y causa a los programadores emociones mucho más positivas.

No pienses que el idioma se ha vuelto más fácil. Todavía puede llamarse uno de los lenguajes de programación más complejos y ampliamente utilizados, si no el más complejo. Pero el C ++ moderno se ha vuelto mucho más amigable que antes.



Hoy hablaremos sobre algunas de las nuevas características del lenguaje (comenzando con C ++ 11, que, por cierto, ya tiene 8 años), que cualquier programador sabrá.

Palabra clave automática


Desde que la palabra clave auto apareció en C ++ 11, la vida de los programadores se ha vuelto más fácil. Gracias a esta palabra clave, el compilador puede generar tipos de variables en tiempo de compilación, lo que nos evita tener que especificar siempre los tipos nosotros mismos. Esto resultó ser muy conveniente, por ejemplo, en casos en los que tiene que trabajar con tipos de datos como map<string,vector<pair<int,int>>> . Al usar la palabra clave auto , hay algunas cosas a tener en cuenta. Considere un ejemplo:

 auto an_int = 26; //    ,     - int auto a_bool = false; //   bool auto a_float = 26.04; //   float auto ptr = &a_float; //       auto data; // #1     ?    - . 

Preste atención a la última línea en este ejemplo, cuyo comentario está marcado como #1 (en adelante, de manera similar marcaremos las líneas que analizaremos después de los ejemplos). No hay inicializador en esta línea, no puede hacer esto. El código ubicado en esta línea evita que el compilador sepa cuál debería ser el tipo de la variable correspondiente.

Inicialmente, la palabra clave auto en C ++ era bastante limitada. Luego, en las versiones más recientes del idioma, se agregaron funciones auto . Aquí hay otro ejemplo:

 auto merge(auto a, auto b) //            auto! {   std::vector<int> c = do_something(a, b);   return c; } std::vector<int> a = { ... }; // #1 -  std::vector<int> b = { ... }; // #2  -  auto c = merge(a,b); //       

Las líneas n. #1 y n #2 aplican la inicialización variable mediante llaves (otra característica nueva en C ++ 11).

Recuerde que cuando usa la palabra clave auto , el compilador debe tener alguna forma de inferir el tipo de la variable.

Ahora, una pregunta interesante. ¿Qué sucede si usa un diseño como auto a = {1, 2, 3} ? Que es esto ¿Vector o causa de error de compilación?

De hecho, una construcción de la forma std::initializer_list<type> apareció en C ++ 11. La lista de valores de inicialización entre paréntesis se tratará como un contenedor con la palabra clave auto .

Y finalmente, como ya se mencionó, la inferencia de tipos por parte del compilador puede ser extremadamente útil si tiene que trabajar con estructuras de datos complejas. Aquí hay un ejemplo:

 void populate(auto &data) { //    !   data.insert({"a",{1,4}});   data.insert({"b",{3,1}});   data.insert({"c",{2,3}}); } auto merge(auto data, auto upcoming_data) { //         auto result = data;   for(auto it: upcoming_data) {       result.insert(it);   }   return result; } int main() {   std::map<std::string, std::pair<int,int>> data;   populate(data);   std::map<std::string, std::pair<int,int>> upcoming_data;   upcoming_data.insert({"d",{5,3}});   auto final_data = merge(data,upcoming_data);   for(auto itr: final_data) {       auto [v1, v2] = itr.second; // #1               std::cout << itr.first << " " << v1 << " " << v2 << std:endl;   }   return 0; } 

Echa un vistazo a la línea #1 . La expresión auto [v1,v2] = itr.second representa una nueva característica de C ++ 17. Esta es la llamada descomposición al declarar variables. En versiones anteriores del lenguaje, cada valor tenía que extraerse individualmente. Gracias a este mecanismo, realizar tales operaciones se ha vuelto mucho más conveniente.

Además, si necesita trabajar con datos usando enlaces, es suficiente agregar solo un carácter a esta construcción, convirtiéndolo a la siguiente forma: auto &[v1,v2] = itr.second .

Expresiones lambda


C ++ 11 introduce soporte para expresiones lambda. Se asemejan a funciones anónimas en JavaScript, se pueden comparar con objetos funcionales sin nombres. Capturan variables en varios ámbitos según su descripción, para lo cual se utilizan construcciones sintácticas compactas. Además, se pueden asignar a variables.

Las expresiones lambda son una herramienta muy útil para aquellos casos en los que necesita realizar una pequeña operación en el código, pero no desea escribir una función separada para esto. Otro ejemplo común de su uso es la creación de funciones utilizadas en la comparación de valores. Por ejemplo:

 std::vector<std::pair<int,int>> data = {{1,3}, {7,6}, {12, 4}}; //        std::sort(begin(data), end(data), [](auto a, auto b) { //   - auto!   return a.second < b.second; }); 

Puedes encontrar muchas cosas interesantes en este breve ejemplo.

Primero, preste atención a lo conveniente que es usar la inicialización variable usando llaves. A continuación, podemos ver las construcciones estándar begin() y end() , que también aparecieron en C ++ 11. Luego viene la función lambda, que se utiliza como mecanismo para comparar datos. Los parámetros de esta función se declaran usando la palabra clave auto , esta característica apareció en C ++ 14. Anteriormente, esta palabra clave no se podía usar para describir los parámetros de las funciones.

Ahora observe que la expresión lambda comienza con corchetes - [] . Esta es la llamada máscara de variables. Determina el alcance de la expresión, es decir, le permite controlar la relación de la expresión lambda con variables y objetos locales.

Aquí hay un extracto de este repositorio dedicado a las características modernas de C ++:

  • [] - la expresión no captura nada. Esto significa que en una expresión lambda es imposible usar variables locales del ámbito externo. Solo se pueden usar parámetros en la expresión.
  • [=] - la expresión captura los valores de los objetos locales (es decir, variables locales, parámetros). Esto significa que se pueden usar, pero no modificar.
  • [&] - la expresión captura referencias a objetos locales. Se pueden modificar, como se muestra en el siguiente ejemplo.
  • [this] : la expresión captura el valor del puntero this .
  • [a, &b] : la expresión captura el valor del objeto b una referencia al objeto b .

Como resultado, si dentro de la función lambda necesita convertir los datos a otro formato, puede usar los mecanismos anteriores. Considere un ejemplo:

 std::vector<int> data = {2, 4, 4, 1, 1, 3, 9}; int factor = 7; for_each(begin(data), end(data), [&factor](int &val) { //    factor     val = val * factor;   factor--; // #1   - ,  -    factor   }); for(int val: data) {   std::cout << val << ' '; // 14 24 20 4 3 6 9 } 

Aquí, si el valor accediera a la variable del [factor] (entonces la máscara variable [factor] se usaría para describir la expresión lambda), entonces en la línea #1 el valor del factor no podría cambiarse, simplemente porque no tendríamos derechos para realizando tal operación. En este ejemplo, tenemos derecho a tales acciones. En tales situaciones, es importante no abusar de las capacidades que las variables de acceso proporcionan por referencia.

Además, tenga en cuenta que val también se accede por referencia. Esto asegura que los cambios de datos que ocurren en la función lambda afectan al vector .

Expresiones de inicialización variable dentro de construcciones if y switch


Realmente me gustó esta innovación de C ++ 17 justo después de descubrirlo. Considere un ejemplo:

 std::set<int> input = {1, 5, 3, 6}; if(auto it = input.find(7); it==input.end()){ //   - ,  -    std::cout << 7 << " not found" << std:endl; } else {   //    else      it   std::cout << 7 << " is there!" << std::endl; } 

Resulta que ahora puede inicializar las variables y compararlas con su uso en un bloque if o switch . Esto ayuda a escribir código preciso. Aquí hay una descripción esquemática de la estructura en consideración:

 if( init-statement(x); condition(x)) {   //    } else {   //     x   //    } 

Realización de cálculos en tiempo de compilación utilizando constexpr


La constexpr nos brinda grandes oportunidades. Supongamos que tenemos algún tipo de expresión que necesita ser calculada, mientras que su valor, después de inicializarlo con la variable correspondiente, no cambiará. Tal expresión puede calcularse de antemano y usarse como una macro. O, lo que se hizo posible en C ++ 11, use la constexpr .

Los programadores se esfuerzan por minimizar la cantidad de cómputo realizado durante la ejecución del programa. Como resultado, si se pueden realizar ciertas operaciones durante el proceso de compilación y, por lo tanto, eliminar la carga del sistema durante la ejecución del programa, esto tendrá un buen efecto en el comportamiento del programa durante la ejecución. Aquí hay un ejemplo:

 #include <iostream> constexpr long long fact(long long n) { //       constexpr return n == 1 ? 1 : (fact(n-1) * n); } int main() { const long long bigval = fact(20); std::cout<<bigval<<std::endl; } 

Este es un ejemplo muy común del uso de constexpr .

Como constexpr la función para calcular el factorial como constexpr , el compilador puede calcular previamente el valor fact(20) en el momento de la compilación del programa. Como resultado, después de la compilación, la cadena const long long bigval = fact(20); puede ser reemplazado por const long long bigval = 2432902008176640000; .

Tenga en cuenta que el argumento pasado a la función está representado por una constante. Esta es una característica importante de usar funciones declaradas usando la constexpr . Los argumentos que se les pasan también deben declararse con la constexpr o con la palabra clave const . De lo contrario, dichas funciones se comportarán como funciones ordinarias, es decir, durante la compilación, sus valores no se calcularán de antemano.

Las variables también se pueden declarar utilizando la constexpr . En este caso, como puede suponer, los valores de estas variables deben calcularse en tiempo de compilación. Si esto no se puede hacer, se mostrará un mensaje de error de compilación.

Es interesante observar que más tarde, en C ++ 17, aparecieron las construcciones constexpr-if y constexpr-lambda .

Estructuras de datos de tupla


Al igual que la estructura de datos de pair , la estructura de datos de tuple (tupla) es una colección de valores de diferentes tipos de un tamaño fijo. Aquí hay un ejemplo:

 auto user_info = std::make_tuple("M", "Chowdhury", 25); //  auto     //    std::get<0>(user_info); std::get<1>(user_info); std::get<2>(user_info); //  C++ 11     tie std::string first_name, last_name, age; std::tie(first_name, last_name, age) = user_info; //  C++ 17, ,       auto [first_name, last_name, age] = user_info; 

A veces, en lugar de una estructura de datos de tuple , es más conveniente usar std::array . Esta estructura de datos es similar a las matrices simples utilizadas en el lenguaje C, equipadas con características adicionales de la biblioteca estándar de C ++. Esta estructura de datos apareció en C ++ 11.

Inferir automáticamente el tipo de argumento de plantilla de clase


El nombre de esta característica parece bastante largo y complejo, pero de hecho no hay nada complicado aquí. La idea principal aquí es que en C ++ 17, la salida de tipos de argumentos de plantilla también se realiza para plantillas de clase estándar. Anteriormente, esto solo era compatible con plantillas funcionales. Como resultado, resulta que solían escribir así:

 std::pair<std::string, int> user = {"M", 25}; 

Con el lanzamiento de C ++ 17, esta construcción ahora se puede reemplazar con esto:

 std::pair user = {"M", 25}; 

La inferencia de tipos se realiza implícitamente. Este mecanismo es aún más conveniente de usar cuando se trata de tuplas. Es decir, antes tenía que escribir lo siguiente:

 std::tuple<std::string, std::string, int> user ("M", "Chy", 25); 

Ahora lo mismo se ve así:

 std::tuple user2("M", "Chy", 25); 

Vale la pena señalar que estas características no parecerán algo digno de atención para aquellos que no están particularmente familiarizados con las plantillas de C ++.

Punteros inteligentes


Trabajar con punteros en C ++ puede ser una verdadera pesadilla. Gracias a la libertad que el lenguaje le da al programador, a veces es muy difícil para él, como dicen, "no dispararse en el pie". En muchos casos, los punteros están presionando para que ese "disparo" del programador.

Afortunadamente, C ++ 11 introdujo punteros inteligentes que son mucho más convenientes que los punteros regulares. Ayudan al programador a evitar pérdidas de memoria al liberar recursos cuando sea posible. Además, proporcionan una garantía de seguridad para excepciones.

Resumen


Aquí hay un buen repositorio, que, creemos, será interesante para aquellos que siguen las innovaciones de C ++. Algo nuevo aparece constantemente en este idioma. Aquí tocamos solo algunas características modernas del lenguaje. De hecho, hay muchos de ellos. Es posible que aún hablemos de ellos.

Estimados lectores! ¿Qué características modernas de C ++ encuentra más interesantes y útiles?

Source: https://habr.com/ru/post/451870/


All Articles