13

Leyendo sobre el lenguaje C++ he visto que algunos desarrolladores hablan de C++17 y no me queda claro si es lo mismo que C++ o es otro lenguaje.

¿Qué es C++17?

PaperBirdMaster
  • 44,474
  • 6
  • 44
  • 82

1 Answers1

23

¡C++17 es C++!.

Es la sexta revisión del Lenguaje de Programación C++, no se llama C++6 porque las revisiones no se numeran por el orden de revisión si no por el año en que la Organización Internacional para la Estandarización (ISO) dan el visto bueno a la revisión del lenguaje (también conocida como Dialecto del Lenguaje de Programación).

¿Qué hubo antes de C++17?

Este es un breve resumen de la historia del lenguaje C++:

  • 1985: Primera implementación comercial de C++.
  • 1989: Segunda versión de C++.
  • 1998: Primera ratificación ISO del lenguaje. Primer dialecto oficial de C++ conocido como C++98.
  • 2003: Segunda revisión del lenguaje, conocida como C++03.
  • 2005: Primera colección de documentos técnicos (Technical Report) conocida como TR1.
  • 2007: Se decide añadir las propuestas del TR1 al estándar, esta tercera revisión del lenguaje se conoce cmo C++TR1.
  • 2011: Cuarta revisión del lenguaje, conocida como C++11.
  • 2014: Quinta revisión del lenguaje, conocida como C++14.
  • 2017: Sexta revisión del lenguaje, conocida como C++17.

¿Qué añade C++17?

Así pues, C++17 no es más que una revisión del lenguaje C++ ¿Qué añade esta revisión respecto a las versiones anteriores?

Deducción de parámetros en objetos plantilla.

Los objetos plantilla pueden deducir los parámetros de plantilla de forma parecida a cómo se deducen en las funciones plantilla:

template <typename T>
void funcion(T t) { /* ... */ }

template <typename T>
struct objeto
{
    objeto(T t) : valor{t} {}
    T valor;
};

int main()
{
    funcion(0);   // Correcto, deduce int t
    funcion('Z'); // Correcto, deduce char t

    objeto o1(0);   // Correcto, deduce int valor. Antes de C++17 es error.
    objeto o2('Z'); // Correcto, deduce char valor. Antes de C++17 es error.

    return 0;
}

Sin embargo, esta deducción no funciona exactamente igual que la deducción de plantillas función, pues no da soporte a deducción parcial.

Deducción de tipos en parámetros plantilla no-tipo.

Se puede usar auto como parámetro plantilla para que la plantilla deduzca el tipo subyacente del parámetro no-tipo facilitado:

//        vvvv <-- Antes de C++17 no se permite usar auto aqui.
template <auto T>
void funcion() { /* ... */ }

static int i = 0;

int main()
{
    funcion<0>();    // Correcto, T sera de tipo int con valor 0
    funcion<'Z'>();  // Correcto, T sera de tipo char con valor 'Z'
    funcion<&i>();   // Correcto, T sera de tipo int * con valor 0x????????

    funcion<3.14>(); /* Error, T deberia ser de tipo double,
    pero double y float no se permiten como parametro no-tipo */
    funcion<int>();  // Error, se esperaba un valor no un tipo.

    return 0;
}
Permitir typename en plantillas-plantilla.

Al declarar una plantilla-plantilla ahora se puede usar indistintamente typename o class (antes no se podía):

//      Correcto en C++17 y anterior --> vvvvv
template <typename T, template<typename> class P>
void funcion1(P<T>) { /* ... */ }
//   Correcto solo a partir de C++17 --> vvvvvvvv
template <typename T, template<typename> typename P>
void funcion2(P<T>) { /* ... */ }
Expresiones plegables.

Es posible aplicar ciertos operadores sobre paquetes de parámetros en plantillas variadicas:

template <typename ... args>
auto funcion(args ... a) { return (a + ...); }
//          Expresion plegada --> ^^^^^^^^^
int main()
{
    // Devuelve 21
    std::cout << funcion(1, 2, 3, 4, 5, 6); // Se despliega en return 1 + 2 + 4 + 4 + 5 + 6;
    // Devuelve hola mundo
    std::cout << funcion("hola"s, ' ', "mundo"s); // Se despliega en return "hola"s + ' ' + "mundo"s;

    return 0;
}

Los operadores plegables son: +, -, *, /, %, ^, &, |, ~, =, <, >, <<, >>, +=, -=, *=, /=, %=, ^=, &=, |=, <<=, >>=, ==, !=, <=, >=, &&, ||, ,, .* y ->* y pueden desplegarse así:

  ( expresion operador ... )
  ( ... operador expresion )
  ( expresion operador ... operador expresion )
Nuevas normas de deducción para valores entre llaves ({}).

Cuando entre llaves sólo haya un valor se deducirá como valor, no como lista:

auto a{'a'}; // a es de tipo char, antes deducia std::initializer_list<char> de un elemento.
auto b{0xb}; // b es de tipo int, antes deducia std::initializer_list<int> de un elemento.
auto c{1.f}; // c es de tipo float, antes deducia std::initializer_list<float> de un elemento.
Expandir paquetes de parámetros en cláusulas using.

Teniendo un objeto base como el siguiente:

template <typename T>
struct base
{
    void f(T) { std::cout << __PRETTY_FUNCTION__ << '\n'; }
};

Y un objeto que deriva de varios base:

template <typename ... T>
struct derivado : base<T> ...
{
};

Podemos traer al ámbito de derivado todas las versiones de base<T>::f(T) con una cláusula using:

template <typename ... T>
struct derivado : base<T> ...
{
    using base<T>::f ...;
};

derivado<int, double, char> d;
/* Equivale a:
using base<int>::f;
using base<double>::f;
using base<char>::f; */
Lambdas expresión constante.

Si la lambda cumple los requisitos para ser una expresión constante, podrá usarse como tal:

// vvvvvvvvv <-- No se puede usar constexpr aqui antes de C++17
   constexpr auto cinco = [](){ return 5; };

int main(int argc, char **)
{
    switch (argc)
    {
//           vvvvvvv <-- Valido de C++17 en adelante
        case cinco(): /* ... */ break;
        default: std::cout << /* ... */ break;
    }

    return 0;
}

Podría omitirse constexpr en la declaración de la lambda pues serán expresiones constantes por defecto si cumplen con los requisitos para serlo.

Capturar *this en lambdas.

En una lambda declarada dentro de un método de un objeto, las capturas genéricas capturan this como puntero:

struct objeto
{
    void f()
    {
//                v <-- Captura this por referencia
        auto a = [&]() { valor = 100 ; }; // Equivale a this->valor = 100;
//                v <-- Captura this por copia
        auto b = [=]() { valor = 200 ; }; // Equivale a this->valor = 200;
//                vvvvv <-- Copia el objeto
        auto c = [*this]() mutable { valor = 300 ; } // Equivale a this->valor = 300;

        a(); // this->valor contiene 100
        b(); // this->valor contiene 200
        c(); /* this->valor no se modifica, contiene 200.
                Se modifica el this->valor de una copia */
    }
    int valor;
};
Atributo [[fallthrough]].

Indica al compilador que la ausencia de una instrucción break al final de un case en un switch es intencionada:

switch (variable)
{
    case 0:
        // Hacer cosas...
    case 1: // <-- ALarma! falta break!
        // Hacer cosas...
        [[fallthrough]];
    case 2: // <-- Ok, la falta de break es intencionada.
        // Hacer cosas...
        break;
}
Atributo [[nodiscard]].

Indica al compilador que un tipo de dato o el retorno de una función no debe ser descartado.

//     vvvvvvvvvvvvv <-- Se aconseja no descartar este dato
struct [[nodiscard]] informacion { /* ... */ };
struct datos { /* ... */ };

informacion obtener_informacion() { /* ... */ }
   [[nodiscard]] datos obtener_datos() { /* ... */ }
// ^^^^^^^^^^^^^ <-- Se aconseja no descartar este retorno
int main()
{
    obtener_informacion(); // Alarma! Se esta descartando un objeto de tipo informacion!
    obtener_datos(); // Alarma! Se esta descartando el retorno de obtener_datos!
    return 0;
}
Atributo [[maybe_unused]].

Indica al compilador que una entidad podría no estar siendo usada intencionadamente.

[[maybe_unused]] void debug() { std::cout << "Debug\n"; }

void f([[maybe_unused]] int valor)
{
    std::cout << "F!\n";
}

El código anterior no mostrará alarmas por no usar la función debug ni por no usar el valor valor.

Atributos en espacios de nombres y enumerados.

Es posible aplicar atributos sobre espacios de nombres y valores enumerados:

namespace [[deprecated]] utiles
{
void f() { /* ... */ }
}

enum puerto
{
    defecto = 8080,
    entrada = 8016,
    [[deprecated]] salida = 8015
};

void datos(puerto p) { /* ... */ }

int main()
{
    utiles::f(); // Alarma! el espacio de nombres f esta deprecado
    datos(defecto);
    datos(salida); // Alarma! puerto::salida esta deprecado
    return 0;
}
Variables inline.

Es posible hacer que las variables se comporten como funciones inline, esto es: considerar múltiples definiciones idénticas de la variable como la misma definición.

Espacios de nombres anidados.

El espacio de nombres:

namespace Primero
{
namespace Segundo
{
namespace Tercero
{
// Cosas...
}
}
}

Puede ser declarado como

namespace Primero::Segundo::Tercero
{
// Cosas...
}
Simplificación de static_assert.

Ya no es obligatorio añadir un texto a static_assert.

Especificaciones de excepciones.

A partir de C++17 si una función no dispone de cláusula throw en la firma se considera que no lanza excepciones.

Emparejamiento estructurado.

Para funciones con múltiples valores de retorno (Que devuelvan std::pair, std::tuple, std::array y agregados) es posible obtener cada valor de retorno por separado:

std::map<int, int> mii;
// i y b declarados por separado:
std::map<int, int>::iterator i;
bool b;
//       +---- Asigna el retorno->first a i y el retorno->second a b
//       |     Valido en C++17 y anterior.
//   vvvvvvvvv
std::tie(i, b) = mii.insert({1, 2});

/* iterador e insertado declarados en la misma línea, su tipo se deduce
de retorno->first y retorno->second respectivamente. Asigna el
retorno->first iterador y el retorno->second a insertado.
valido solo a partir de C++17 */
auto [iterador, insertado] = mii.insert({3, 4});
//   ^^^^^^^^^^^^^^^^^^^^^ <--- Emparejamiento estructurado

struct abc { int a, b, c; };
abc f1() { return {2, 3, 5}; }
std::tuple<std::string, float, abc, int> f2() { return {"Hola mundo", 3.14f, {1, 2, 3}, 4}; }
std::array<int, 5> f3() { return {0, 1, 2, 3, 4}; }

// Asigna retorno.a a x, retorno.b a y, retorno.c a z
auto [x, y, z] = f1();
//   ^^^^^^^^^ <--- Emparejamiento estructurado

// Asigna 1r elemento de la tupla a str, segundo a pi, tercero a xyz y cuarto a v
auto [str, pi, xyz, v] = f2();
//   ^^^^^^^^^^^^^^^^^ <--- Emparejamiento estructurado

// Asigna retorno[0] a p0, retorno[1] a p1...
auto [p0, p1, p2, p3, p4] = f3();
//   ^^^^^^^^^^^^^^^^^^^^ <--- Emparejamiento estructurado
Inicializador en estructuras de control.

Es posible declarar e inicializar variables en if y switch:

if (auto [iterador, insertado] = mii.insert({3, 4}); !insertado)
    std::cout << "Elemento " << iterador->first << " repetido!\n";
else
    std::cout << "Elemento " << iterador->first << "anyadido con valor " << iterador->second << '\n';

switch (int i = f1() + f2(); i)
{
    /* ... */
}
Bucle for de rango generalizado.

Ahora se permite que begin y end en el for de rango sean de diferentes tipos.

Condicion if constante.

Se puede añadir constexpr a la cláusula if. Si la condición de la condición constante no se cumple el código dentro de dicha condición no se compilará.

#define DEBUG false

if constexpr (DEBUG)
{
    // Codigo que debe ser compilado solo en modo DEBUG
}
WIP....

Añadiendo más cosas.

PaperBirdMaster
  • 44,474
  • 6
  • 44
  • 82