C++17

C++ 17 en détail : Simplification du code

Bartek's coding

Traducteur

ABD-Z

Avec chaque norme C++, nous visons un code plus simple, plus propre et plus expressif. C++ 17 offre plusieurs « grosses » fonctionnalités de langage qui devraient rendre notre code plus agréable. Allons jeter un coup d’œil à cela.

Commentez

- Titre : C++17

- Auteur : Bartek's coding

- Traducteur : ABD-Z

- Parution : 21 juin 2020

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2020 Bartek's coding . Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Les séries

- III. Déclarations d'affectation structurée (structured binding)

- IV. L'instruction d'initialisation pour les conditions if/switch

- V. Variables inline

- VI. constexpr if

- VII. D'autres fonctionnalités

- VIII. Résumé

I - Introduction

Vous pourriez vous dire que la plupart des nouvelles fonctionnalités du langage (sans parler des améliorations de la bibliothèque standard) sont là pour permettre d'écrire du code plus simple, plus propre. La série « C++ 17 en détail » passe en revue la plupart des choses les plus importantes encore d'actualité. J'ai essayé de choisir quelques fonctionnalités qui rendront votre code plus compact dès le départ.

II - Les séries

Ce billet est le cinquième de la série traitant les détails des fonctionnalités de C++17.

Voici le plan de la série de billets :

- Corrections et dépréciation

- Clarification linguistique

- Les Templates

- Les attributs

- Simplification (la publication ci-dessous)

- Modifications de la librairie - Système de fichiers

- Modifications de la librairie - Algorithmes parallèles

- Modifications de la librairie - Utils

- Récapitulez, Bonus - avec un ebook gratuit ! :)

Documents et Liens

Pour rappel

Si vous souhaitez, dans un premier temps, vous plonger par vous-même dans la norme du C++17, vous pouvez lire ce dernier document de travail : N4659, 2017-03-21, Working Draft, Standard for Programming Language C++ - le lien apparaît également sur ce site : isocpp.org.

Vous pouvez également récupérer ma liste décrivant de manière concise toutes les fonctionnalités du langage C++17.

« Download a free copy of my C++17 Cheat Sheet! »

Il s'agit d'une page de référence en PDF.

Liens

- Prise en charge du compilateur : C++ compiler support.

- Document officiel avec modifications : P0636r0: Changes between C++14 and C++17 DIS.

- Conférence de Bryce Lelbach : C++Now 2017: C++17 Features.

- Mon billet précédent traitant les fonctionnalités de C++17 : C++17 Features.

- Jason Turner : C++ Weekly channel où il a couvert la plupart (même entièrement !) des fonctionnalités de C++17.

Très bien, maintenant, commençons à traiter ces fonctionnalités !

III - Déclarations d'affectation structurée (structured binding)

Est-ce que vous travaillez souvent avec les tuples ?

Si ce n'est pas le cas, vous devez alors commencer à vous y intéresser. Non seulement les tuples sont sollicités pour retourner plusieurs valeurs d'une fonction, mais en plus, ils reçoivent un support particulier du langage, ce qui rend le code encore plus propre et plus facile à comprendre.

Par exemple (source : std::tie du site cppreference.com) :

std::set<S> mySet;

S value{42, "Test", 3.14};
std::set<S>::iterator iter;
bool inserted;

// unpacks the return val of insert into iter and inserted
std::tie(iter, inserted) = mySet.insert(value);

if (inserted)
std::cout << "Value was inserted\n";

Remarquez que vous êtes contraint de déclarer en premier la variable iter ainsi que la variable inserted pour ensuite utiliser std::tie… C'est quand même pas mal de code.

Avec C++17 :

std::set<S> mySet;

S value{42, "Test", 3.14};

auto [iter, inserted] = mySet.insert(value);

Une ligne au lieu de trois ! C'est aussi plus facile à lire et c'est plus sûr, n'est-ce pas ?

Maintenant, vous pouvez également utiliser const et écrire const auto [iter, inserted] et assurer la cohérence des constantes.

Les affectations structurées ne sont pas uniquement limitées aux tuples, nous avons ci-dessous trois cas d'utilisation :

1. Si l'initialiseur est un tableau :

// works with arrays:
double myArray[3] = { 1.0, 2.0, 3.0 };
auto [a, b, c] = myArray;

2. Si l'initialiseur prend en charge la classe std::tuple_size<size> et fournit de même la fonction get<N>() (dont je pense être le cas le plus commun) :

auto [a, b] = myPair; // binds myPair.first/second

3. En d'autres termes, vous pouvez fournir la prise en charge pour vos classes en supposant le fait d'ajouter une implémentation de la fonction de l'interface get<N>.

4. Si le type de l'initialiseur contient uniquement des membres non static et public :

struct S { int x1 : 2; volatile double y1; };
S f();
const auto [ x, y ] = f();

5. Maintenant c'est plus facile d'avoir la référence d'un membre tuple : auto& [ refA, refB, refC, refD ] = myTuple;
Et voici l'un des usages le plus stylé (prend en charge les boucles !) :

std::map myMap;
for (const auto & [k,v] : myMap)
{
// k – key
// v – value
}

D'ailleurs : affectation structurée ou décomposition de déclaration ?

Concernant cette fonctionnalité, vous avez sûrement vu un autre nom utilisé : « Déclaration Décomposée » (Decomposition Declaration). Comme nous pouvons le voir, ces deux noms ont été pris en compte, cependant la norme retient « affectation structurée ».

Plus de détail dans :

6. Partie : 11.5 Déclaration d'affections déstructurantes [dcl.struct.bind] ;

7. P0217R3 ;

8. P0144R0 ;

9. P0615R0 : Renaming for structured bindings ;

10. C++ today : Structured Binding (C++17 inside) ;

11. C++17 Structured Bindings – Steve Lorimer.

Compatible avec GCC: 7.0, Clang: 4.0, MSVC: VS 2017.3

IV - L'instruction d'initialisation pour les conditions if/switch

Nouvelles versions pour les blocs if et switch en C++ :

if (initialisation; condition) et switch (initialisation; condition).

Dans les versions précédentes, on se doit d'écrire comme ci-dessous :

{
auto val = GetValue();
if (condition(val))
// on success
else
// on false...
}

Remarquez que la portée de val est distincte de celle du bloc if/else puisqu'elle a une portée recouvrant au-delà de la condition.

Désormais vous pouvez écrire :

if (auto val = GetValue(); condition(val))
// on success
else
// on false...

val est visible uniquement à l'intérieur du bloc if/else et donc val ne « fuit » pas au-delà de la condition.

Pourquoi est-ce utile ?

Disons que vous voulez rechercher quelque chose dans une chaîne de caractères :

const std::string myString = "My Hello World Wow";

const auto it = myString.find("Hello");
if (it != std::string::npos)
std::cout << it << " Hello\n"

const auto it2 = myString.find("World");
if (it2 != std::string::npos)
std::cout << it2 << " World\n"

Nous sommes forcés d'utiliser un autre nom pour it ou alors séparer les portées :

{
const auto it = myString.find("Hello");
if (it != std::string::npos)
std::cout << it << " Hello\n"
}

{
const auto it = myString.find("World");
if (it != std::string::npos)
std::cout << it << " World\n"
}

La nouvelle version de if/else permet de séparer les portées en une ligne :

if (const auto it = myString.find("Hello"); it != std::string::npos)
std::cout << it << " Hello\n";

if (const auto it = myString.find("World"); it != std::string::npos)
std::cout << it << " World\n";

Comme dit avant, la variable définie dans l'instruction if est également visible dans le bloc else. Vous pouvez donc écrire ceci :

if (const auto it = myString.find("World"); it != std::string::npos)
std::cout << it << " World\n";
else
std::cout << it << " not found!!\n";

En plus,vous pouvez l’utiliser avec une affectation structurée :

// better together: structured bindings + if initializer
if (auto [iter, succeeded] = mymap.insert(value); succeeded) {
use(iter);  // ok
// ...
} // iter and succeeded are destroyed here

Le C++ ne devient-il pas de plus en plus Pythonic ? – comme rédigé dans l'une des publications du blog de Jeff Preshing ? :)

Plus de détails dans :

- P0305R1 ;

- C++ Weekly - Ep 21 C++17’s if and switch Init Statements ;

- Python TUPLE - Pack, Unpack, Compare, Slicing, Delete, Key.

Compatible avec GCC: 7.0, Clang: 3.9, MSVC: VS 2017.3.

V - Variables inline

Avec les initialisations de membres de données non statiques (voir ce post : http://www.bfilipek.com/2015/02/non-static-data-members-initialization.html), nous pouvons maintenant déclarer et initialiser des variables membres en un seul endroit. Cependant, quant aux variables statiques (ou constantes statiques), elles doivent être généralement définies dans un fichier .cpp.

C++11 et le mot-clef constexpr vous permettent de déclarer et de définir des variables statiques en un seul endroit, mais ceci est uniquement limité à l'expression contexpr.

Bien, mais quel est l'intérêt de cette fonctionnalité ?

Dans les versions précédentes de C++, uniquement les méthodes et les fonctions peuvent être assignées inline, maintenant il est possible de le faire également pour les variables dans un fichier d'en-tête.

Une variable déclarée inline a la même sémantique qu'une fonction déclarée inline : elle peut être définie, à l'identique, dans plusieurs unités de traduction (fichier .cpp ou .hpp) ou, doit être définie dans chaque unité de traduction dans laquelle elle est utilisée, et le comportement du programme est comme s'il y avait exactement une variable.

struct MyClass
{
static const int sValue;
};

inline int const MyClass::sValue = 777;

Ou même plus simple :

struct MyClass
{
inline static const int sValue = 777;
};

Notez aussi que les variables contexpr sont de nature inline implicitement, il n'y a alors aucune nécessité d'utiliser constexpr inline myVar = 10;.

Pourquoi cela simplifie-t-il le code ?

Par exemple, un bon nombre de bibliothèques entièrement implémentées par fichier d’en-tête peuvent limiter le nombre de bidouilles (comme utiliser les fonctions inline ou template) en utilisant uniquement des variables inline.

L'avantage par rapport à constexpr est que l'initialisation de l'expression n'a pas à être constexpr.

Plus d'informations ici :

- P0386R2 ;

- SO: What is an inline variable and what is it useful for?

Compatible avec GCC : 7.0, Clang : 3.9, MSVC : pas encore.

VI - constexpr if

J'ai déjà introduit cette fonctionnalité dans mon précédent post concernant les templates : templates/contexpr-if (http://www.bfilipek.com/2017/06/cpp17-details-templates.html#constexpr-if), la description de la fonctionnalité étant brève, il est temps de présenter des exemples qui mettront davantage en lumière cette fonctionnalité.

Concernant les modèles de code ? Hmm… Comme vous vous en souvenez peut-être constexpr if peut être utilisé pour remplacer des manipulations qui sont déjà faites :

- la technique SFINAE (Substitution Failure Is Not An Error) pour enlever les surcharges de fonction qui ne correspondent pas à l'ensemble des surcharges ;

- vous voudrez peut-être regarder les parties avec std::enable_if de C++14 qui devraient être facilement remplacées par constexpr if ;

- Tag dispatch (technique d'ajout de nouveaux symboles en paramètres du constructeur ou de fonctions).

Donc, dans la majorité des cas, nous pouvons maintenant écrire tout simplement une instruction if constexpr et cela en fera du code plus propre. Ceci est particulièrement important pour la métaprogrammation et les templates qui sont, je pense, complexes de par leur nature.

Un exemple simple, Fibonacci :

template<int  N>
constexpr int fibonacci() {return fibonacci<N-1>() + fibonacci<N-2>(); }
template<>
constexpr int fibonacci<1>() { return 1; }
template<>
constexpr int fibonacci<0>() { return 0; }

Maintenant, il peut être écrit de manière plus compacte (version sans temps de compilation) :

template<int N>
constexpr int fibonacci()
{
if constexpr (N>=2)
return fibonacci<N-1>() + fibonacci<N-2>();
else
return N;
}

Dans C++ Weekly episode 16, Jason Turner expose un exemple montrant que consexpr if ne fait aucun court circuit logique. L'expression entière compile :

if constexpr (std::is_integral<T>::value &&
std::numeric_limits<T>::min() < 10)
{

}

Vous obtiendrez une erreur de compilation si T est un std::string, car numeric_limits n'est pas défini dans les chaînes de caractères.

Il y a un bon exemple dans la vidéo C++Now 2017: Bryce Lelbach “C++17 Features”, à la 16e minute, où constexpr if peut être utilisé pour définir la fonction get<N> – qui pourrait fonctionner avec les affectations structurées.

struct S
{
int n;
std::string s;
float d;
};

template <std::size_t I>
auto& get(S& s)
{
if constexpr (I == 0)
return s.n;
else if constexpr (I == 1)
return s.s;
else if constexpr (I == 2)
return s.d;
}

Contre précédemment, vous devez réécrire les fonctions :

template <> auto& get<0>(S &s) { return s.n; }
template <> auto& get<1>(S &s) { return s.s; }
template <> auto& get<2>(S &s) { return s.d; }

Comme vous pouvez le voir, déterminer le plus simple des deux codes est tout de même débattable. Bien que dans ce cas, nous n'ayons utilisé qu'une structure simple, avec quelques exemples du monde réel, le code final serait beaucoup plus complexe et donc l'usage de constexpr_if serait plus judicieux.

Plus de détails ici :

- C++ Weekly Special Edition - Using C++17’s constexpr if – YouTube - exemples réels de Jason et de ses projects.

- C++17: let’s have a look at the constexpr if – FJ - l'exemple sur Fibonacci s'en inspire de ce site.

- C++ 17 vs. C++ 14 — if-constexpr – LoopPerfect – Medium - contient beaucoup d'exemples intéressants

Compatible avec GCC: 7.0, Clang: 3.9, MSVC: VS 2017.3

VII - D'autres fonctionnalités

Nous pouvons affirmer que la plupart des nouvelles fonctionnalités du langage C++ simplifient le langage d'une manière ou d'une autre. Dans cette publication, je me suis concentré sur les points les plus importants, sans aussi trop répéter.

Cependant, juste pour rappel, prenez en considération également les fonctionnalités suivantes permettant aussi de rendre le code plus simple :

- template<auto> - voir ici ;

- Fold Expressions - déjà mentionné dans mon précédent post ;

- Déduction d'argument de template pour les templates de classe - mentionné ici.

Sans oublier une multitude de fonctionnalités de la bibliothèque ! Nous les évoquerons plus tard.

VIII - Résumé

À mon avis, C++17 fait de vrais progrès tendant vers du code compact, expressif et facile à comprendre.

L'une des meilleures nouveautés est constrexpr if permettant l'écriture de templates et de métaprogrammation de manière similaire à du code standard. Selon moi, c'est un gros bénéfice (car j'ai toujours peur des trucs effrayants des templates).

La seconde fonctionnalité : les affectations structurées (qui fonctionnent même pour les boucles) rendant le code proche d'un langage dynamique tel que Python.

Comme vous le voyez, toutes les fonctionnalités précédemment mentionnées sont déjà implémentées dans les compilateurs GCC et Clang. Si vous travaillez avec les versions récentes de ces compilateurs, vous pouvez dès à présent expérimenter ces nouvelles caractéristiques avec C++17. Bientôt, un bon nombre de ces fonctionnalités seront disponibles dans VS 2017.3.

- Quelles sont les meilleures caractéristiques et fonctionnalités de C++17 qui rendent le code plus propre ?

- Avez-vous déjà joué avec constexpr if ou les affectations structurées ?

Pour maintenant, nous avons parcouru l'essentiel des fonctionnalités de C++17, il est donc temps de passer à de nouvelles choses dans la Standard Library.