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. Con C++ se puede crear casi cualquier tipo de programa: juegos, controladores de dispositivos, computación de alto rendimiento, aplicaciones en la nube, de escritorio, sistemas embebidos y aplicaciones móviles, y mucho más. Incluso las bibliotecas y compiladores para otros lenguajes de programación se escriben en C++.

Uno de los requisitos originales para C++ era la compatibilidad con el lenguaje C. Como resultado, C++ permite la programación de estilo C, con punteros sin procesar, matrices, cadenas de caracteres terminadas en NULL y otras características. Pueden habilitar un rendimiento excelente, pero también pueden generar errores y complejidad.

La evolución de C++ enfatiza las características que reducen considerablemente la necesidad de usar 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, como la memoria del montículo, los descriptores de archivo y los sockets, deben estar controlados por 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 RAII, la biblioteca estándar de C++ proporciona tres tipos de puntero inteligentes:

Un puntero inteligente controla la asignación y eliminación de la memoria que posee. 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 unique_ptr clase encapsula las llamadas a new y delete. Cuando un widget objeto sale del ámbito, se invocará el unique_ptr destructor y 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, utilice un puntero inteligente para gestionar la memoria dinámica. 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 obtendrá la ventaja de disponer de funciones miembro para buscar, añadir, anteponer, etc. 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. Proporcionan iteradores para recorrer los elementos de forma segura. Están altamente optimizados para el rendimiento y se prueban exhaustivamente para la corrección. 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 necesite la optimización del rendimiento, considere la posibilidad de usar:

  • Contenedores asociativos desordenados, como unordered_map. Estos contenedores tienen una menor sobrecarga por elemento y búsqueda en tiempo constante, pero pueden ser más difíciles de usar correctamente y eficazmente.
  • 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 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 por defecto, junto con bucles for basados en rangos.
  • transform: para la modificación sin reemplazo de los elementos de un 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, usa un < estricto y lambdas con nombre cuando puedas.

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 arrays y contenedores es propensa a errores de indexación y además es tediosa de escribir. Para eliminar estos errores y hacer que el código sea más legible, utilice bucles for basados en intervalos con contenedores de la biblioteca estándar y arrays nativos. Para obtener más información, consulte la instrucción for basada en intervalos.

#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, se puede usar la inicialización con 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, consulte Inicialización mediante llaves.

Semántica de movimiento

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 el constructor de movimiento de los objetos si está definido. 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 podrían definirse en otro lugar 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 find_if función invoca en cada elemento 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. [=] especifica que esas variables se capturan por valor. En otras palabras, la expresión lambda tiene sus propias copias de esos 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. Las uniones no garantizan la seguridad de tipos y son propensas a provocar 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 de forma segura en cuanto a los tipos.

Consulte también