Aquí está otra vez C++: C++ moderno

Desde su creación, C++ se ha convertido en uno de los lenguajes de programación más utilizados en el mundo. Los programas bien escritos de C++ son rápidos y eficaces. El lenguaje es más flexible que otros lenguajes: puede funcionar en los niveles más altos de abstracción y hacia abajo en el nivel del silicio. C++ proporciona bibliotecas estándar altamente optimizadas. Asimismo, permite el acceso a características de hardware de bajo nivel para maximizar la velocidad y minimizar los requisitos de memoria. C++ puede crear casi cualquier tipo de programa: juegos, controladores de dispositivos, HPC, nube, escritorio, incrustado, aplicaciones móviles, etc. Incluso hay bibliotecas y compiladores de otros lenguajes de programación escritos en C++.

Uno de los requisitos originales para C++ era la compatibilidad con el lenguaje C. Como resultado, C++ siempre ha permitido la programación de estilo C, con punteros básicos, matrices, cadenas de caracteres terminadas en NULL y otras características. Dichas características ofrecen un gran rendimiento, pero también pueden generar errores y complejidades. La evolución de C++ tiene características destacadas que reducen en gran medida la necesidad de utilizar expresiones de estilo C. Las antiguas características de programación C todavía están ahí cuando las necesita. Sin embargo, en código C++ moderno debería necesitarlas cada vez menos. El código C++ moderno es más sencillo, más seguro y más elegante, y tan rápido como siempre.

En las secciones siguientes se proporciona información general sobre las características principales de C++ moderno. A menos que se indique lo contrario, las características que se enumeran aquí están disponibles en C++11 y versiones posteriores. En el compilador de Microsoft C++, puede establecer la opción de compilador /std para especificar qué versión del estándar se usará para el proyecto.

Recursos y punteros inteligentes

Una de las principales clases de errores en la programación de estilo C es la fuga de memoria. A menudo, las fugas se deben a un error al realizar llamadas a delete para memoria que se ha asignado con new. En el código C++ moderno destaca el principio de que la adquisición de recursos es la inicialización (RAII). La idea es sencilla. Los recursos (memoria de montón, identificadores de archivos, sockets, etc.) deben ser propiedad de un objeto. Ese objeto crea o recibe el recurso recién asignado en su constructor y lo elimina en su destructor. El principio de RAII garantiza que todos los recursos se devuelvan correctamente al sistema operativo cuando el objeto propietario salga del ámbito.

Para admitir la adopción sencilla de los principios de RAII, la biblioteca estándar de C++ proporciona tres tipos de puntero inteligente: std::unique_ptr, std::shared_ptr y std::weak_ptr. Un puntero inteligente controla la asignación y la eliminación de la memoria de la que es propietario. En el ejemplo siguiente se muestra una clase con un miembro de matriz que se asigna en el montón en la llamada a make_unique(). La clase unique_ptr encapsula las llamadas a new y delete. Cuando un objeto widget sale del ámbito, se invoca el destructor unique_ptr, el cual liberará la memoria asignada para la matriz.

#include <memory>
class widget
{
private:
    std::unique_ptr<int[]> data;
public:
    widget(const int size) { data = std::make_unique<int[]>(size); }
    void do_something() {}
};

void functionUsingWidget() {
    widget w(1000000);  // lifetime automatically tied to enclosing scope
                        // constructs w, including the w.data gadget member
    // ...
    w.do_something();
    // ...
} // automatic destruction and deallocation for w and w.data

Siempre que sea posible, use un puntero inteligente al administrar memoria en montón. Si debe usar los operadores new y delete explícitamente, siga el principio de RAII. Para obtener más información, vea Duración de objetos y administración de recursos (RAII).

std::string y std::string_view

Las cadenas de estilo C son otra de las principales fuentes de errores. Mediante el uso de std::string y std::wstring, puede eliminar prácticamente todos los errores asociados a las cadenas de estilo C. También podrá aprovechar las ventajas de las funciones miembro para buscar, anexar y anteponer elementos, entre otras tareas. Ambos están muy optimizados para la velocidad. Al pasar una cadena a una función que únicamente requiere acceso de solo lectura, en C++17 puede usar std::string_view para obtener una ventaja de rendimiento incluso mayor.

std::vector y otros contenedores de la biblioteca estándar

Todos los contenedores de la biblioteca estándar siguen el principio de RAII y proporcionan iteradores para el recorrido seguro de los elementos. Además, están muy optimizados para el rendimiento y se han sometido a pruebas exhaustivas para comprobar si son correctos. Mediante el uso de estos contenedores, se elimina la posibilidad de que haya errores o ineficiencias que podrían transferirse a estructuras de datos personalizadas. En lugar de matrices sin formato, use vector como un contenedor secuencial en C++.

vector<string> apples;
apples.push_back("Granny Smith");

Use map (no unordered_map) como contenedor asociativo predeterminado. Use set, multimap y multiset para los casos degenerados y múltiples.

map<string, string> apple_color;
// ...
apple_color["Granny Smith"] = "Green";

Cuando la optimización del rendimiento es necesaria, considere utilizar:

  • Contenedores asociativos desordenados, como unordered_map. Estos tienen una menor sobrecarga por elemento y una búsqueda de tiempo constante, pero pueden ser más difíciles de usar de forma correcta y eficaz.
  • Elementos vector ordenados. Para más información, vea Algoritmos.

No use matrices de estilo C. En el caso de las API más antiguas que necesiten acceso directo a los datos, use mecanismos de acceso como f(vec.data(), vec.size()); en su lugar. Para obtener más información sobre los contenedores, vea Contenedores de la biblioteca estándar de C++.

Algoritmos de biblioteca estándar

Antes de suponer que necesita escribir un algoritmo personalizado para el programa, revise primero los algoritmos de la biblioteca estándar de C++. La biblioteca estándar contiene una serie de algoritmos en constante crecimiento para muchas operaciones comunes, como la búsqueda, la ordenación, el filtrado o la aleatorización. La biblioteca matemática es muy amplia. A partir de C++17 se proporcionan versiones paralelas de muchos algoritmos.

Aquí se describen algunos ejemplos importantes:

  • for_each, el algoritmo de recorrido predeterminado (junto con los bucles for basados en rangos).
  • transform, para la modificación descontextualizada de los elementos de contenedor.
  • find_if, el algoritmo de búsqueda predeterminado.
  • sort, lower_bound, y los demás algoritmos de ordenación y búsqueda predeterminados.

Para escribir un comparador, utilice un elemento < estricto y lambdas con nombre cuando pueda.

auto comp = [](const widget& w1, const widget& w2)
     { return w1.weight() < w2.weight(); }

sort( v.begin(), v.end(), comp );

auto i = lower_bound( v.begin(), v.end(), widget{0}, comp );

auto en lugar de nombres de tipos explícitos

C++11 incluyó por primera vez la palabra clave auto para su uso en declaraciones de variables, funciones y plantillas. auto indica al compilador que deduzca el tipo del objeto para que no tenga que escribirlo explícitamente. auto es especialmente útil cuando el tipo deducido es una plantilla anidada:

map<int,list<string>>::iterator i = m.begin(); // C-style
auto i = m.begin(); // modern C++

Bucles for basados en rangos

La iteración de estilo C sobre matrices y contenedores es propensa a la indexación de errores, además de tediosa de escribir. Para eliminar estos errores y hacer que el código sea más legible, use bucles for basados en rangos con contenedores de la biblioteca estándar y matrices sin formato. Para obtener más información, vea Instrucción for basada en intervalo (C++).

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v {1,2,3};

    // C-style
    for(int i = 0; i < v.size(); ++i)
    {
        std::cout << v[i];
    }

    // Modern C++:
    for(auto& num : v)
    {
        std::cout << num;
    }
}

Expresiones constexpr en lugar de macros

Las macros en C y C++ son tokens procesados por el preprocesador antes de la compilación. Todas las instancias de un token de macro se reemplazan por su valor o expresión definidos antes de la compilación del archivo. Las macros se utilizan normalmente en la programación de estilo C para definir valores constantes en tiempo de compilación. Sin embargo, las macros son propensas a errores y difíciles de depurar. En C++ moderno, debería dar preferencia a las variables constexpr para las constantes en tiempo de compilación:

#define SIZE 10 // C-style
constexpr int size = 10; // modern C++

Inicialización uniforme

En C++ moderno, puede usar la inicialización de llaves para cualquier tipo. Esta forma de inicialización es especialmente útil al inicializar matrices, vectores u otros contenedores. En el ejemplo siguiente, v2 se inicializa con tres instancias de S. v3 se inicializa con tres instancias de S que, a su vez, se inicializan mediante llaves. El compilador infiere el tipo de cada elemento según el tipo declarado de v3.

#include <vector>

struct S
{
    std::string name;
    float num;
    S(std::string s, float f) : name(s), num(f) {}
};

int main()
{
    // C-style initialization
    std::vector<S> v;
    S s1("Norah", 2.7);
    S s2("Frank", 3.5);
    S s3("Jeri", 85.9);

    v.push_back(s1);
    v.push_back(s2);
    v.push_back(s3);

    // Modern C++:
    std::vector<S> v2 {s1, s2, s3};

    // or...
    std::vector<S> v3{ {"Norah", 2.7}, {"Frank", 3.5}, {"Jeri", 85.9} };

}

Para obtener más información, vea Inicialización de llaves.

Semántica de transferencia de recursos

C++ moderno proporciona semántica de transferencia de recursos, lo que permite eliminar copias de memoria innecesarias. En versiones anteriores del lenguaje, las copias eran inevitables en determinadas situaciones. Una operación move transfiere la propiedad de un recurso de un objeto al siguiente sin hacer una copia. Algunas clases son propietarias de recursos como memoria de montón, identificadores de archivo y otros elementos. Cuando se implementa una clase propietaria de recursos, se puede definir un constructor de movimiento y un operador de asignación de movimiento para ella. El compilador elige estos miembros especiales durante la resolución de sobrecargas en situaciones en las que no se necesita una copia. Los tipos de contenedor de la biblioteca estándar invocan al constructor de movimiento en objetos si se define uno. Para obtener más información, vea Constructores de movimiento y operadores de asignación de movimiento (C++).

Expresiones lambda

En la programación de estilo C, se puede pasar una función a otra mediante un puntero de función. El mantenimiento y la comprensión de los punteros de función no es sencilla. La función a la que hacen referencia puede definirse en cualquier parte del código fuente, lejos del punto en el que se invoca. Además, no cuentan con seguridad de tipos. C++ moderno proporciona objetos de función, que son clases que invalidan el operador operator(), lo que permite que se les llame como una función. La forma más práctica de crear objetos de función es con expresiones lambda insertadas. En el ejemplo siguiente se muestra cómo usar una expresión lambda para pasar un objeto de función que la función find_if invocará en los elementos del vector:

    std::vector<int> v {1,2,3,4,5};
    int x = 2;
    int y = 4;
    auto result = find_if(begin(v), end(v), [=](int i) { return i > x && i < y; });

La expresión lambda [=](int i) { return i > x && i < y; } se puede leer como "función que toma un único argumento de tipo int y devuelve un valor booleano que indica si el argumento es mayor que x y menor que y". Observe que las variables x y y del contexto circundante se pueden usar en la expresión lambda. El símbolo [=] especifica que el valor captura esas variables; es decir, la expresión lambda tiene sus propias copias de dichos valores.

Excepciones

C++ moderno destaca las excepciones en lugar de los códigos de error como la mejor manera de notificar y controlar las condiciones de error. Para obtener más información, vea Procedimientos recomendados de C++ moderno para excepciones y control de errores.

std::atomic

Use el struct std::atomic y los tipos relacionados de la biblioteca estándar de C++ para los mecanismos de comunicación entre subprocesos.

std::variant (C++17)

Las uniones se suelen usar en la programación de estilo C para conservar memoria, ya que permiten que los miembros de tipos diferentes ocupen la misma ubicación de memoria. Sin embargo, las uniones no cuentan con seguridad de tipos y son propensas a errores de programación. C++17 incluye por primera vez la clase std::variant como una alternativa más sólida y segura a las uniones. La función std::visit se puede usar para acceder a los miembros de un tipo variant con seguridad de tipos.

Consulte también

Referencia del lenguaje C++
Expresiones lambda
Biblioteca estándar de C++
Conformidad del lenguaje Microsoft C/C++