GCC 8.1 : Nouveautés du langage C++

Introduction

La version 8.1 du compilateur GCC est sortie le 25 Avril 2018. Les nouveautés de cette version pour les différents langages sont disponibles ici. Dans ce post, nous allons voir les nouveautés qui concernent le langage C++.

Support expériemental de certaines fonctionalités du prochain standard C++2a

Le compilateur propose les options -std=c++2a ou std=gnu++2a pour utiliser certaines fonctionalités du standard C++2a en cours de finalisation.

C++2a (langage)

Initialisation par défaut des membres de type bit

Les membres de classes/structures de type bit peuvent avoir leur initialisation par défaut. Même si l’otpion -stc=c++2a n’est pas utilisée, le compilateur acceptera l’instruction avec un message d’avertissement.

default_init_bit_field.cpp:7:15: warning: default member initializers for bit-fields only available with -std=c++2a or -std=gnu++2a
     int x : 3 = 2;

Capture [=, this]

Cette fonctionalité mineure autorise l’utilisation de la syntaxe suivante [=, this] qui était considérée comme une erreur. Elle est équivalente à la syntaxe [=] mais son autorisation permet de faire la différence plus clairement avec la syntaxe [=, *this].

Les pointeurs vers des membres const par référence

Cette fonctionalité corrige un problème dans l’interprétation des pointeurs vers membres qui sont const &.

Avant de voir le changement, commençons par utiliser un membre ref qualified

struct foo
{
    void f() &;
};

Cette déclaration interdit d’avoir un appel de ce type

  foo{}.f();

Par contre on peut faire ceci:

struct foo
{
    void f() const & {};
};

int main(int argc, char *argv[])
{
    foo{}.f();
}

Par contre, lors de l’utilisation de .* avec un objet temporaire (ou pour être plus précis une rvalue), le langage interdisait d’avoir un pointeur vers une fonction membre avec le qualifier &. Ceci donnait une incohérence entre l’interprétation de deux instructions équivalentes.

struct foo
{
    void f() const & {};
};

int main(int argc, char *argv[])
{
    foo{}.f();
    (foo{}.*(&foo::f))();
}

La compilation du programme ci-dessous donnait le résultat suivant:

error: pointer-to-member-function type 'void (foo::*)() const &' requires an lvalue
     (foo{}.*(&foo::f))();

La norme c++2a permet de remédier à cette incohérence, et les deux syntaxes sont acceptées. A noter que l’utilisation de l’option -std=c++2a n’est pas nécessaire avec gcc pour compiler le programme précédent.

Simplication des règles pour les boucles sur une plage (range-based loop)

Quand un type définit un membre begin mais pas de membre end (ou l’inverse), l’utilisation des boucle de plage est impossible (jusqu’au prochain standard C++20). Avec ce changement, on peut définir des méthodes begin/end externes et les utiliser.

Pour illustrer ce changement, considérons la classe suivante (avec les deux opérations begin/end définies)

struct foo{
  foo() {
    generate(&tab[0], &tab[10], [n = 0]() mutable { return n++; });
  }

  int *begin() { return &tab[0]; }
  int *end() { return &tab[10]; }

  int tab[10];
  int past_last;
};

int main(int argc, char *argv[]) {
    foo f;
    for (auto i : f) { cout << i << "/"; }
    cout << endl;
}

Si on enlève les deux méthodes et qu’on définisse des méthodes externes, le programme reste correct.

struct foo{
  foo() {
    generate(&tab[0], &tab[10], [n = 0]() mutable { return n++; });
  }

  int tab[10];
  int past_last;
};

int *begin(foo &f) { return &f.tab[0]; }
int *end(foo &f) { cout << "y"; return &f.tab[10]; }

int main(int argc, char *argv[]) {
    foo f;
    for (auto i : f) { cout << i << "/"; }
    cout << endl;
}

Par contre si on enlève seulement une des deux méthodes, le programme devient incorrect.

struct foo{
  foo() {
    generate(&tab[0], &tab[10], [n = 0]() mutable { return n++; });
  }

  // int *begin() { return &tab[0]; }
  int *end() { return &tab[10]; }

  int tab[10];
  int past_last;
};

int *begin(foo &f) { return &f.tab[0]; }
int *end(foo &f) { return &f.tab[10]; }

int main(int argc, char *argv[]) {
    foo f;
    for (auto i : f) { cout << i << "/"; }
    cout << endl;
}

Avec ce changement, il est possible d’avoir une des deux méthodes membres begin/end définie et d’utiliser les boucle de plage en définissant les deux méthodes à l’extérieur de la classe.

Clarification de la déduction des règles de déduction des arguments des templates

Avec la déduction des paramètres des template, une incohérence est apparue avec les constructeurs acceptant une liste et en particulier lors de leur utilisation avec un seul élément.

Par exemple avec la définition ci-dessous, le type déduit est vector<vector<int>.

    vector v{vector{1, 2, 3}};

Cette déduction peut être perçue comme incohérente lorsqu’une définition similaire utilise le constructeur par copie comme ci-dessous (déduction de tuple<int, int, int>).

    tuple t{tuple{1, 2, 3}};

Avec ce changement, lorsqu’un seul élément est présent dans la liste, le type déduit est vector<int>. La même chose s’applique au type list.

C++2a (libstdc++)

  1. Implémentation expériementale des fonction std::to_address et std::endian.
  2. Implémentation du type std::experimental::source_location : voici un exemple d’utilisation
#include <iostream>
#include <experimental/source_location>

using namespace std;

int main(int, char *[]) {
  experimental::source_location l = experimental::source_location::current();
  cout << l.file_name() << "/" << l.function_name() << "/" << l.line() << "/"
       << l.column() << endl;
}

Amélioration de la gestion des erreurs de compilation

Accès à un membre privé lorsqu’un accesseur est disponible

Cette fonctionalité permet si on accède à un membre privé d’afficher l’accesseur correspondant. Un exemple montrant son Utilisation

struct foo
{
public:
    int get_x() const { return x; }
private:
    int x;
};

int main(int, char*argv[]) {
    foo f;
    cout << f.x << endl;
}
gcc8_getter.cpp: In function 'int main(int, char**)':
gcc8_getter.cpp:15:15: error: 'int foo::x' is private within this context
     cout << f.x << endl;
               ^~
gcc8_getter.cpp:10:9: note: declared private here
     int x;
         ^~
gcc8_getter.cpp:15:15: note: field 'int foo::x' can be accessed via 'int foo::get_x() const'
     cout << f.x << endl;
               ^~
      get_x()

Utilisation d’un macro utilisée avant sa définition

Cette aide à la compilation permet d’indiquer si la macro a été utilisée avant sa définition.

const int x = 3 * PI;

#define PI 3.14

int main(int, char*argv[]) {
}

Cast

L’option -Wold-style-cast permet d’afficher un message d’aide pour indique quand utiliser static_cast, ‘const_cast’ ou ‘reinterpret_cast’

struct base {};
struct foo : public base {};
int main(int, char *argv[]) {
  {
    base *b = new foo;
    foo *f = (foo *)b;
    delete b;
  }
  {
    const foo f1;
    foo &f2 = (foo &)f1;
  }
}
gcc8_cast.cpp: In function 'int main(int, char**)':
gcc8_cast.cpp:8:21: warning: use of old-style cast to 'struct foo*' [-Wold-style-cast]
     foo *f = (foo *)b;
                     ^
              --------
              static_cast<foo *> (b)
gcc8_cast.cpp:13:22: warning: use of old-style cast to 'struct foo&' [-Wold-style-cast]
     foo &f2 = (foo &)f1;
                      ^~
               ---------
               const_cast<foo &> (f1)

Erreur dans un bloc extern "C"

Lorsqu’une erreur à l’intérieur d’un bloc extern "C" est détectée, le compilateur affiche le début du bloc extern "C".

Arguments des templates non concordants

Lorsque le compilateur détecte une différence des types des paramètres d’un template, le compilateur met en évidence le type différent et remplace les paramètres concordants par trois points dans le message d’erreur.

Il y a aussi une nouvelle option -fdiagnostics-show-template-tree qui permet d’afficher les templates qui ne correspondent pas sous forme d’arbre.

LIBSTDC++

  1. Implémentation de l’API système de fichier std::filesystem
  2. std::char_traits et std::char_traits peuvent être utilisé dans des expressions constantes
  3. Implémentation partielles des fonction std::to_chars et std::from_chars Ces fonctions intégrées à la norme C++17 ont été partiellement implémentées car elles ne supportent actuellement que les types entiers.
#include <array>
#include <charconv>
#include <system_error>
#include <iostream>
#include <string_view>

using namespace std;

int main(int, char*[]){
    array<char, 10> str;
    auto res = to_chars(str.data(), str.data() + str.size(), 42);
    if (res.ec == errc()) cout << string_view(str.data(), res.ptr - str.data());
}
#include <array>
#include <charconv>
#include <iostream>
#include <string_view>

using namespace std;

int main(int, char*[]){
    array<char, 10> str{"42"};
    int res;
    from_chars(str.data(), str.data() + str.size(), res);
    cout << res << endl;
}

Divers

L’option -Wreturn-type activée par défaut

L’option -Wreturn-type est activée par défaut, ce qui n’était pas le cas dans gcc7

int foo(const bool b) {
  if (b) return 1;
}

L’option -Wclass-memaccess incluse dans -Wall

Cette option de gestion des avertissements permet d’avertir lorsque des classes complexes sont manipulées avec des fonctions de gestion de mémoire telle memcpy. Cette option est incluse dans l’option -Wall

struct foo1 {
  int x = 0;
  double y = 0;
};
struct foo2 {
  int x;
  double y;
  virtual void f() {}
};

struct foo3 {
    int x;
    vector<int> v;
};

int main(int, char*[])
{
    foo1 f1a, f1b;
    memcpy(&f1a, &f1b, sizeof(foo1));

    foo2 f2a, f2b;
    memcpy(&f2a, &f2b, sizeof(foo2));

    foo3 f3a, f3b;
    memcpy(&f3a, &f3b, sizeof(foo3));
}

Le résultat avec l’option activée est le suivant (seul le type simple foo1 est accepté )

memaccess.cpp:28:36: warning: 'void* memcpy(void*, const void*, size_t)' writing to an object of type 'struct foo2' with no trivial copy-assignment; use copy-assignment or copy-initialization instead [-Wclass-memaccess]
     memcpy(&f2a, &f2b, sizeof(foo2));
                                    ^
memaccess.cpp:11:8: note: 'struct foo2' declared here
 struct foo2 {
        ^~~~
memaccess.cpp:31:36: warning: 'void* memcpy(void*, const void*, size_t)' writing to an object of type 'struct foo3' with no trivial copy-assignment; use copy-assignment or copy-initialization instead [-Wclass-memaccess]
     memcpy(&f3a, &f3b, sizeof(foo3));
                                    ^
memaccess.cpp:17:8: note: 'struct foo3' declared here
 struct foo3 {
        ^~~~
Tools  gcc  CPP