| Pour contribuer à ce document, merci de lire le README.md
|————————————————————————-
Les nouveautés au cœur du C++17
Auteurs | Oliver H, Adrien Jeser, Guillaume Dua, olibre, eggman, Yves Bourguignon, Storm, gorbal, palm123, khivapia, BAud, Segfault, Benoît Sibaud, Lucas, cracky, Martin Peres, et RyDroid |
---|---|
License | CC BY-SA 4.0 |
URL | https://linuxfr.org/news/nouveautes-au-coeur-du-cpp17 |
Date | 2016-07-22T00:53:12+02:00 |
Tags | c++17, c++ et cpp |
Score | 0 |
L’ajout des fonctionnalités au C++17 a été clôturé au premier semestre 2016. Depuis, nous nous efforçons à vous fournir des dépêches de qualité sur le sujet. Après deux dépêches de mise-en-bouche, cette troisième dépêche entre enfin dans le vif du sujet en décortiquant les changements au niveau du langage C++. Quelques anecdotes parsèment cet article, des suggestions comme le C++ without class en écho au C with classes, ou quelques illustrations inédites comme celle du « Compilé c’est testé, linké c’est livré ». Alors faisons donc le tour des nouveautés :-)
- La précédente dépêche « C++17, Genèse d’une version mineure »
- Contenu markdown de cette dépêche sur le dépôt C++FRUG
- La prochaine dépêche « C++17 élargit la bliothèque standard »
- Réunion du comité en mars 2016 racontée par Herb Sutter
- Réunion du comité en juin 2016 racontée par Herb Sutter
- Réunion du comité en mars 2016 détaillée par botondballo
- Liste très complète des nouveautés C++17 (licence CC BY-SA 3.0) par Yakk et contributeurs
- Liste des nouveautés C++17 sur Meeting C++
- Dépôts Git du comité de standardisation ISO C++)
- Article Wikipédia C++17
- Les principaux apports de C++11/14/17 (licence MIT) par Anthony Calandra et autres contributeurs
TODO
Reste à faire avant publication :
Qui ? | Quoi ? |
---|---|
??? | Ajouter des images (humoristiques) pour illustrer les sous-sections (s’inspirer des précédentes dépêches) |
Adrien | Déplacer dans la dépêche suivante le TS Alias de iostream |
Oliver | Créer la dépêche Bilan C++17 dans l’espace de rédaction LinuxFr.org |
Oliver | Créer la dépêche Faut-il continuer à apprendre le C++ ? dans l’espace de rédaction LinuxFr.org |
Oliver | |
??? | Simplifier le § [P0036] Révision de la TS précédente N4295 |
Oliver | Des statistiques: la dépêche la plus longue de LinuxFr.org … |
Adrien | Passer un coup de Grammalect sur tout les paragraphes pour insérer les espaces insécables (et autres formatages de texte) |
??? | Relire le chapitre Fonctionnalités majeures non intégrées. Devrait-il en dire un peu plus ? |
??? | Supprimer les textes transférés des nouvelles dépêches |
Série de dépêches C++
Cette dépêche fait partie de toute une série disponible également sur le dépôt Git du Groupe des Utilisateurs C++ Francophone (C++FRUG). Alors que cet article restera figé sur le site LinuxFr. org, il continuera d’évoluer sur le dépôt Git. Merci de nous aider à maintenir ce document à jour avec vos questions/suggestions/corrections.
- Les coulisses du standard (20 août 2016) nous dévoile des aspects biens souvent méconnus des développeurs C++ de la naissance du langage aux derniers soubresauts de son processus de normalisation.
- Genèse du C++17 (2 oct. 2016) rappelle la longue maturation des fonctionnalités du C++17 et dresse le périmètre fonctionnel amendé par deux réunions du comité de standardisation C++ en 2016.
- Cette troisième dépêche décrypte les spécifications techniques (TS) concernant le langage C++.
- La suite, Changements C++17 au niveau de la bibliothèque standard comme son nom l’indique s’attaque aux
std ::*
. - Bilan C++17 et attentes pour C++20 Version mineure ou majeure ? Et pour C++20 ?
- Faut-il apprendre le C++ ? compare le C++ aux alternatives et permet de mieux situer l’intérêt du C++ dans le monde sans cesse mouvant de l’informatique.
- …
Date | Dépêche |
---|---|
20 août 2016 | Les coulisses du standard de la naissance aux processus de normalisation. |
2 oct. 2016 | Genèse du C++17 lors des deux réunions du comité de standardisation |
?? déc. 2016 | C++17 étend le cœur du langage avec des nouveautés ne pouvant pas être réalisées facilement |
?? déc. 2016 | lambda et [[attribut]] |
?? déc. 2016 | C++17 au goût sucré avec beaucoup de sucre syntaxique au cœur du langage |
?? déc. 2016 | Grand nettoyage du C++ |
?? ???? 2017 | bibliothèque standard |
?? ???? 2017 | bibliothèque standard |
?? ???? 2017 | bibliothèque standard |
?? ???? 2017 | bibliothèque standard |
?? ???? 2017 | bibliothèque standard |
?? ???? 2017 | Bilan C++17 Version mineure ou majeure ? |
?? ???? 2017 | Attentes C++20 Et pour C++20 ? Comment participer ? |
?? ???? 2017 | Faut-il continuer à apprendre le C++ ? Alternatives ? Intérêt du C++ dans le monde sans cesse mouvant de l’informatique ? |
Améliorations notables
[P0061] __has_include
La macro __has_include()
vérifie si l’en-tête est disponible pour inclusion.
#if __has_include(<filesystem>)
# include <filesystem>
#elif __has_include(<experimental/filesystem>)
# include <experimental/filesystem>
#elif __has_include(<boost/filesystem.hpp>)
# include <boost/filesystem.hpp>
#else
# error Ne trouve aucune en-tête filesystem
#endif
Allons plus loin avec un autre exemple :
#if __has_include(<windows.h>)
# include <windows.h>
LONGLONG ticks1nano = []() {
LARGE_INTEGER freq;
QueryPerformanceFrequency(&freq);
return freq.QuadPart / 1000'000;
}();
LONGLONG nanosecondes() {
LARGE_INTEGER time;
QueryPerformanceCounter(&time);
return time.QuadPart/ticks1nano;
}
#elif __has_include(<time.h>)
# include <time.h>
auto nanosecondes() {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC,&ts);
return 1000'000'000 * ts.tv_sec + ts.tv_nsec;
}
#else
# error Ne trouve ni <windows.h> ni <time.h>
#endif
[P0135] Court-circuitage du constructeur par copie (Guaranteed copy elision)
C++17 garantit le court-circuitage du constructeur par copie. Mais pas dans tous les cas : ce TS distingue le cas général « elision » du cas spécifique « genuine elision ».
int n = 0; // compte le nombre d’appel du constructeur par copie
struct A
{
explicit A(int) {}
A(const A&) { ++n; }
};
int main()
{
A a = A( A( A( A(42) ) ) );
return n; // toujours 0 avec C++17
} // pas de garantie avant C++17
Les expressions sont catégorisées suivant cette taxonomie :
TODO [Faire un schéma]
[P0145] Fixer l’ordre d’évaluation des expressions
Anecdote
Le livre mythique The C++ Programming Language de l’inventeur du C++, Bjarne Stroustrup contient une erreur subtile à la page 1046 du paragraphe 36.3.6 STL-like Operations (quatrième édition publiée en 2013). Sauras-tu la retrouver ? Voici l’extrait en question :
The
replace()
replaces one substring with another and adjusts thestring
’s size accordingly. For example:void f() { string s = "but I have heard it works even if you don't believe in it"; s.replace(0,4,""); // erase initial "but " s.replace(s.find("even"),4,"only"); s.replace(s.find(" don't"),6,""); // erase by replacing with "" assert(s=="I have heard it works only if you believe in it"); }
A
replace()
returns a reference to the object for which it was called. This can be used for chaining operations:void f2() { string s = "but I have heard it works even if you don't believe in it"; s.replace(0,4,"").replace(s.find("even"),4,"only").replace(s.find(" don't"),6,""); assert(s=="I have heard it works only if you believe in it"); }
Pas trouvé ? Pas d’inquiétude, aucun humain n’avait trouvé cette erreur. Ce n’est que bien après la publication que cette erreur a été trouvée, pas par une nouvelle forme d’Intelligence Artificielle, mais juste un outil d’analyse statique de code source au nez et à la barbe des pointures C++ aguerries.
Explication
Pour des questions de performance, le standard C++ (avant C++17) indique que c’est le compilateur qui optimise l’ordre d’évaluation du chaînage et des paramètres de fonction. Le standard utilise le terme unsequenced (séquencement non défini). Le C et le C++ partagent ensemble cette règle.
Donc, l’expression replace(find()). replace(find())
dans la fonction f2()
peut être évaluée dans des ordres différents. En théorie, la variable s
peut contenir différents résultats. Et en pratique aussi :
Compilateur | Résultat contenu par la variable s |
---|---|
GCC et MSVC |
I have heard it works evenonlyyou donieve in it |
LLVM/Clang | I have heard it works only if you believe in it |
Détails
Ci-dessous, la première ligne déclare et initialise un objet std::string
. La seconde ligne cherche et remplace plusieurs caractères de cette std::string
en utilisant le chaînage des fonctions replace
.
std::string s = "but I have heard it works even if you don't believe in it";
s.replace(0,4,"").replace(s.find("even"),4,"only").replace(s.find(" don't"),6,"");
Intuitivement, on s’attendrait à évaluer les arguments des fonctions comme find("even")
juste avant d’appeler replace(résultat,4,"only")
. Mais ce n’est pas le cas avant C++17, ces arguments peuvent être évalués dans différents ordres. Plus de détails sont donnés par Shafik Yaghmour (en Anglais).
Le tableau ci-dessous présente sur chacune des sept lignes, un ordre d’appel possible selon les standards C++ (avant C++17) et C (en supposant que ce soit une struct string
avec des pointeurs de fonction).
1er appel | 2e appel | 3e appel | 4e appel | 5e appel |
---|---|---|---|---|
find(" don't") |
find("even") |
replace(0,4,"") |
replace(f,4,"only") |
replace(f,6,"") |
find("even") |
find(" don't") |
replace(0,4,"") |
replace(f,4,"only") |
replace(f,6,"") |
find(" don't") |
replace(0,4,"") |
find("even") |
replace(f,4,"only") |
replace(f,6,"") |
find("even") |
replace(0,4,"") |
find(" don't") |
replace(f,4,"only") |
replace(f,6,"") |
replace(0,4,"") |
find(" don't") |
find("even") |
replace(f,4,"only") |
replace(f,6,"") |
replace(0,4,"") |
find("even") , find(" don't") |
replace(f,4,"only") |
replace(f,6,"") |
|
replace(0,4,"") |
find("even") |
replace(f,4,"only") |
find(" don't") |
replace(f,6,"") |
C++17 n’autorise qu’une seule possibilité, la dernière, et correspond à :
s.replace(0, 4, "");
auto p = s.find("even");
s.replace(p, 4, "only");
auto q = s.find(" don't");
s.replace(q, 6, "");
Pour info, cet exemple du livre The C++ Programming Language a justement été repris par le standard C++ (brouillon de juillet 2016), au §5.2.2 Function call (page 107).
Autres exemples
Par exemple dans l’expression f().g(h())
les fonction f()
peut-être appelée avant ou après h()
. Le standard C++ fait la différence entre unspecified (non-spécifié) et unsequenced (non-séquencé). Ce comportement est bien spécifié, donc Avant C++17 c’est unsequenced. À partir de C++17, c’est f()
avant g()
(sequenced before).
// Avant C++17, f() peut être appelée avant ou après h()
f().g( h() );
// C++17 est plus intuitif : f() est toujours appelée avant h()
C’est aussi le cas de l’expression std::cout << f() << g() << h()
qui peut appeler ces trois fonctions dans n’importe quel ordre.
// Avant C++17, le compilateur décide l’ordre d’évaluation de f(), g() et h()
std::cout << f() << g() << h() << std::endl;
// C++17 fixe l’ordre intuitif : d’abord f(), puis g() et enfin h()
Deux autres exemples que le C++ partage avec le langage C :
std::map<int, int> m;
m[0] = m.size();
std::cout << m[0]; // Affiche 0 ou 1 ?
// Clang : 0
// GCC : 1
// MSVC++ : 0
int i = 0;
std::cout << i << ' ' << i++;
// Clang : 0 0
// GCC : 1 0
// MSVC++ : 1 0
Conséquence
Donc, beaucoup de codes sont potentiellement truffés de ces pièges, ce qui est également le cas quand std::future<T>
est utilisé. Tout le monde se fait avoir, débutants comme experts. Même les meilleurs experts C++ se font avoir avec ces règles pas toujours intuitives. Et le comité de normalisation C++ a donc décidé de fixer l’ordre d’évaluation avec ce TS.
Nouvelle règle
L’évaluation est :
- De la gauche vers la droite pour les expressions suffixées. Ceci inclue les appels de fonction et la section des membres ;
- L’assignement de la droite vers la gauche (
a = b = c = d
) ; - Les opérateurs de décalage (shift operators) de la gauche vers la droite.
Par contre, lorsque une surcharge d’opérateur est invoquée, la priorité arithmétique est utilisée. Peut-être que le code généré sera moins performant, mais le langage gagne grandement en simplicité.
Expression | Résultat avant C++17 | Avec C++17 |
---|---|---|
std::map<int,int> m; m[0] = m.size(); |
unsequenced | m[0] = ?? |
int i = 0, ++i++, ++i++, ++i++, ++i, i++; |
i = 8 |
i = 8 |
int i = 0; i = i++ + 1; |
unsequenced | unsequenced |
[P0400] Corrige la précédente TS P0145
Une toute petite reformulation d’une phrase de la TS concernant l’ordre d’évaluation des arguments de fonction.
[P0245] Littéral pour exprimer la virgule flottante en hexadécimal
La réunion de Jacksonville en février 2016 a amendé ce TS qui permet d’exprimer les virgules flottantes (IEEE 754) en hexadécimal. Enfin, le C++ permet d’avoir une représentation exacte des virgules flottantes. Cette fonctionnalité était déjà présente depuis longtemps dans d’autres langages : C99, Java 5 (2004)…
La représentation hexadécimale a l’avantage d’être celle du registre (mémoire binaire). Attention à la notation décimale des virgules flottantes. Par exemple, 0.1f
ne vaut pas exactement 0.1
mais 0.10000000149…
. Un exemple :
#include <stdint.h> // int64_t
#include <iostream> // std::cout
int main()
{
float un_dixieme = 0.1f;
float f_1e11 = un_dixieme * 1e12f; // Erreur d'arrondi
int64_t i_1e11 = 0.1 * 1e12; // Pas d'erreur d'arrondi
double diff = f_1e11;
diff -= i_1e11; // soustraction f_1e12 - i_1e11
std::cout.precision(99);
std::cout <<
"un_dixieme = "<< un_dixieme << "\n"
"f_1e12 = "<< f_1e11 << "\n"
"i_1e12 = "<< i_1e11 << "\n"
"diff = "<< diff << '\n';
}
Qui donne le résultat :
un_dixieme = 0.100000001490116119384765625
f_1e12 = 99999997952
i_1e12 = 100000000000
diff = -2048
Les hexadécimaux permettent d’écrire la représentation exacte des virgules flottantes en s’affranchissant de ces erreurs d’arrondis. Passons à la pratique :
// Fraction hexadécimale
float v = 0xa.bp3f;
assert(v == 85.5f);
// 0xA.B = 0xA*16^0 + 0xB*16^-1
// 0xA = 10
// 0x.B = 11/16 = 0,6875
// 0xA.B = 10,6875
// p3 = 2^3 = 8
// v = 10,6875*8 = 85,5
// 'f' final = type 'float'
double w = 0xC0DE2017.1CAFEp-1;
assert(w == 1617891339.55602931976318359375);
// 0xC0DE2017 = 3235782679
// 0x1CAFE = 117502
// 0xFFFFF = 1048576
// p-1 = 2^-1 = 1/2
// w = (3235782679 + 117502/1048576) / 2
Concours
Chère lectrice, cher lecteur LinuxFr.org, as-tu d’autres idées de jeux de mots avec cette notation hexadécimale ? Alors défoule toi dans les commentaires ;-) Les plus beaux jeux de mots seront récompensés par des petits cadeaux (goodies) collectés lors du dernier salon Paris Open Source Summit (16 et 17 novembre 2016) et seront envoyés par courrier électronique postal ;-)
Remarquons le p
à la fin. Celui-ci représente l’exposant binaire et il est suivi par un entier décimal (et non pas hexadécimal). Cet exposant binaire est obligatoire pour plusieurs raisons :
- Évite l’ambiguïté du
f
final dans0xA.Bf
(float
ou le chiffreF
hexadécimal ?) ; - Évite l’ambiguïté du
E
dans0xa.bE-12
(exposant-12
ou0xA.BE - 12
?) ; - Corresponds à la norme IEEE 754 (puissance de deux) ;
- 100% compatible avec la notation C99 (et celle d’autres langages).
Tentons de représenter cette notation hexadécimale en regex :
0[xX][0-9a-fA-F]+[.]?[pP][+-]?[0-9]+[fFlL]?
0[xX][0-9a-fA-F]*[.][0-9a-fA-F]+[pP][+-]?[0-9]+[fFlL]?
Termes du standard
Allez, soyons curieux, regardons comment le standard C++ spécifie cette notation avec un extrait du chapitre § 2.13.4 Floating literals du brouillon C++17 :
hexadecimal-floating-literal: hexadecimal-prefix hexadecimal-fractional-constant binary-exponent-part floating-suffixopt hexadecimal-prefix hexadecimal-digit-sequence binary-exponent-part floating-suffixopt hexadecimal-fractional-constant: hexadecimal-digit-sequenceopt . hexadecimal-digit-sequence hexadecimal-digit-sequence . binary-exponent-part:
p
signopt digit-sequenceP
signopt digit-sequence sign: one of+
-
digit-sequence: digit digit-sequence ’opt digit floating-suffix: one off
l
F
L
Et l’équivalent chez cppreference.com :
0x | 0X hex-digit-sequence
0x | 0X hex-digit-sequence .
0x | 0X hex-digit-sequence(optional) . hex-digit-sequence
Hexadecimal digit-sequence representing a whole number without a radix separator. The exponent is never optional for hexadecimal floating-point literals:
0x1ffp10
,0X0p-1
,0x1.p0
,0xf.p-1
,0x0.123p-1
,0xa.bp10l
The exponent syntax for hexadecimal floating-point literal has the form
p | P exponent-sign(optional) digit-sequence
exponent-sign, if present, is either + or -
suffix, if present, is one of
f
,F
,l
, orL
. The suffix determines the type of the floating-point literal:
- (no suffix) defines double
f F
defines floatl L
defines long double
strtof()
et std::hexfloat
En attendant C++17, il est possible d’utiliser strtof()
et std::hexfloat
pour jouer avec les virgules flottantes hexadécimales :
#include <iostream>
#include <cstdlib>
#include <cstdio>
int main (int argc, char *argv[])
{
if (argc != 2) {
std::cout <<"Usage: "<< argv[0] <<" 0xA.Bp-1 => Decode hexfloat" "\n";
return 1;
}
std::cout <<"Decode floating point hexadecimal = "<< argv[1];
long double l = std::strtold(argv[1],NULL); if(errno==ERANGE)std::cout<<"\nstrtold() erreur";
double d = std::strtod (argv[1],NULL); if(errno==ERANGE)std::cout<<"\nstrtod() erreur";
float f = std::strtof (argv[1],NULL); if(errno==ERANGE)std::cout<<"\nstrtod() erreur";
std::cout <<"\n" "long double = "<< std::defaultfloat << l <<'\t'<< std::hexfloat << l
<<"\n" "double = "<< std::defaultfloat << d <<'\t'<< std::hexfloat << d
<<"\n" "float = "<< std::defaultfloat << f <<'\t'<< std::hexfloat << f
<<'\n';
}
Nous pouvons regretter qu’il faille utiliser des fonctions strtof()
issues du C, qui imposent de verifier si errno == ERANGE
. En théorie, std::hexfloat
devrait fonctionner pour l’entrée (istream
). Mais dans la pratique std::hexfloat
semble ne fonctionner que pour la sortie (ostream
). L’exemple suivant ne fonctionne toujours pas avec GCC-6.2, Clang-3.9 et MSVC++15 :
double d;
std::istringstream iss("0xA.Bp-1");
iss >> std::hexfloat >> d;
std::cout << d;
Notons que c’est l’extraction qui ne s’effectue pas correctement. L’istringstream
reste quand à lui dans un état correct, ainsi les erreurs sont vérifiables.
std::cout
<< std::boolalpha
<< iss.fail() << '\n' // false
<< iss.bad() << '\n' // false
<< iss.eof() << '\n' // false
<< iss.str() << '\n';// "0xA.Bp-1"
[P0292] if constexpr
À l’origine (fin 2011), c’était static_if
. Puis renommé en constexpr_if
. Et détaché en deux mots constexpr if
. Finalement, les deux mots sont inversés pour donner if constexpr
. Cette fonctionnalité va simplifier beaucoup de code générique :
// Trois définitions nécessaires avant C++17
void fonction()
{ std::cout << std::endl; }
template <class T>
void fonction (const T& t)
{ std::cout << t; }
template <class T, class... R>
void fonction (const T& t, const R&... r)
{
fonction(t); // Gère un argument
fonction(r...); // Gère le reste
}
// Une seule définition grâce à C++17
template <class T, class... R>
void fonction (const T& t, const R&... r)
{
std::cout << t; // Gère un argument
if constexpr (sizeof...(r))
fonction(r...); // Gère le reste
else
std::cout << std::endl;
}
[P0386] Variable inline
Les variables inline
— comme les fonctions inline
— peuvent être définies dans plusieurs unités de traduction (translation units). Son utilisation suggère au compilateur de substituer l’appel à la variable par son contenu. Elles sont adéquates pour remplacer les macros non-triviales. Plus subtil, on peut contourner la règle de la définition unique (One Definition Rule).
struct A
{
// Déclaration de la variable inline
static int v;
};
// Définition de la variable inline
inline int A::v = 0;
// Ou plus simplement
struct B
{
static inline int v = 0;
};
// Les variables constexpr sont implicitement inline
constexpr const int celerite_lumiere = 299'792'458;
En C++14, pour cette syntaxe avec inline
, le compilateur produit ces erreurs :
error C2433: 'B::v': 'inline' not permitted on data declarations
error C2864: 'B::v': a static data member with an in-class initializer must have non-volatile const integral type
note: type is 'int'
Dorénavant, l’uniformité de syntaxe est acceptée, indépendamment de la présence d’un CV-qualifier.
Par curiosité, générons le code assembleur x86_64 (syntaxe NASM) de l’exemple ci-dessus avec et sans variable inline
. Le compilateur clang++ -S --std=c++1z -O0
optimise davantage le code avec variable inline
en supprimant les lignes suivantes :
.type _ZN1A27variable_inline_sans_inlineE,@object
.section .rodata,"a",@progbits
.globl _ZN1A27variable_inline_sans_inlineE
.p2align 2
_ZN1A27variable_inline_sans_inlineE:
.long 42
.size _ZN1A27variable_inline_sans_inlineE, 4
Améliorations concernant les attributs
Trois nouveaux attributs standards : [[fallthrough]]
, [[nodiscard]]
et [[maybe_unused]]
. Initialement, ces trois nouveaux attributs étaient portés par un seul TS, le P0068. Ces attributs complètent les [[noreturn]]
, [[carries_dependency]]
et [[deprecated]]
introduits par C++11 et C++14.
[P0188] [[fallthrough]]
Le terme anglais fall through désigne le fait de passer à travers (entre les mailles du filet, tomber à l’eau…). Cette expression était déjà utilisée comme convention pour signifier l’intention de continuer avec le prochain case
dans un switch
(c’est à dire de ne pas terminer le case
avec un break
).
switch (valeur)
{
case 1:
std::cout <<" valeur 1";
break;
case 2: // fall through
case 3:
std::cout <<" valeur 2 ou 3";
break;
default:
std::cout <<" valeur différente"
" de 1, 2, 3, 4";
}
Donc, comme son nom l’indique, l’attribut [[fallthrough]]
indique au compilateur (ou à l’outil d’analyse statique de code) que c’est normal qu’il n’y ait pas de break;
à la fin d’un case
, on continue bien avec le case
suivant. Cela évite des avertissements (warnings) inutiles.
switch (valeur)
{
case 1:
std::cout <<" valeur 1";
break;
case 2: // Pas de break
[[fallthrough]]; // => Continue avec
// le case suivant
case 3:
std::cout <<" valeur 2 ou 3";
break;
case 4:
std::cout <<" valeur 4";
// Ici pas de break ni de [[fallthrough]]
// Un oubli du développeur ?
// => Le compilateur avertit
default:
std::cout <<" valeur différente"
" de 1, 2, 3 et 4";
}
[P0189] [[nodiscard]]
L’attribut [[nodiscard]]
indique que la valeur de retour d’une fonction ne doit pas être ignorée. Ce fonctionnement était déjà implémenté par l’extension GNU __attribute__((warn_unused_result))
.
// Ancienne version de affiche_division()
// avec une faille : la division par zéro
//void affiche_division (int a)
//{ std::cout << (42/a); }
// Nouvelle version
// Le code de retour doit être
// pris en compte par l'appelant
[[nodiscard]]
bool affiche_division (int a)
{
if (a == 0)
return false; // échec
std::cout << (42/a);
return true; // succès
}
// Mais main() n'a pas été mise à jour !
int main (int argc, char *argv[])
{
affiche_division(argc-1);
// Le compilateur avertit que
} // le code de retour est ignoré
[P0212] [[maybe_unused]]
L’attribut [[maybe_unused]]
(qui devait s’appeler [[unused]]
) permet de supprimer des avertissements (warning) quand une variable, une fonction ou un paramètre de fonction n’est pas utilisé. Notons que la spécification C++ traite de plus en plus des avertissements (warning), auparavant seules les erreurs des compilateurs étaient spécifiées.
std::mutex g_mutex;
[[maybe_unused]]
void affiche_division(int a)
{
[[maybe_unused]]
std::lock_guard<std::mutex> lock(g_mutex);
std::cout << (42/a);
}
int main (int argc, [[maybe_unused]] char *argv[])
{
[[maybe_unused]]
bool test = argc % 2;
assert(test); // assert() désactivé en
} // mode Release (-DNDEBUG)
// Pour éviter les warnings avant C++17
#ifdef DOXYGEN_PARSING
# define DOC_ONLY(x) x
#else
# define DOC_ONLY(x)
#endif
#ifdef NDEBUG
# define DEBUG_ONLY(x) // Release
#else
# define DEBUG_ONLY(x) x // Debug
#endif
int main (int argc, char** DOC_ONLY(argv))
{
DEBUG_ONLY(bool impaire = argc % 2);
assert(impaire);
}
Mozilla propose aussi DebugOnly<T>
(plus élégant qu’une macro).
[N4266] Appliquer les [[attributs]]
aux namespace
et enum
Ce TS corrige un oubli du C++14 et permet d’appliquer des [[attributs]]
aux namespace
et enum
.
[[maybe_unused]]
namespace debug
{
enum Couleur {
Rouge,
Vert,
Web [[deprecated("Ce n'est pas une couleur")]],
Bleu
};
}
[P0028] using
évite la répétition de namespace d’[[attribut]]
s
Répétition du namespace rpr
pour les deux attributs :
void foo() {
[[rpr::kernel, rpr::target(cpu,gpu)]]
bar();
}
Utilise le même namespace rpr
pour les deux attributs :
void foo() {
[[using rpr: kernel, target(cpu,gpu)]]
bar();
}
[P0283] Ignorer les [[attributs]]
non reconnus
Avec C++17, les compilateurs doivent désormais ignorer les attributs qu’ils ne connaissent pas. Avant, les compilateurs pouvaient rejeter les attributs ne faisant pas partie du standard.
Améliorations concernant les lambdas
Les lambdas ont été introduites avec C++11. C++14 a amélioré leur utilisation, notamment en permettant les variadiques lambdas. Et C++17 apporte deux autres améliorations.
[N4487] Lambda constexpr
Les lambda sont automatiquement constexpr
si répond aux critères constexpr
. Cela permet d’utiliser les lambdas dans les fonctions constexpr
:-)
[P0018] Capture de *this
Dans une lambda, accéder aux membres de this
se faisait toujours via le pointeur, comme this->membre
. C++17 permet une capture d’une copie de *this
avec la syntaxe [=,copie=*this]
. Tout le contenu de l’objet *this
est copié (capturé) au lieu de seulement du pointeur this
. C’est très avantageux pour les petits objets. La copie d’un objet plus volumineux permet aussi d’éviter de mettre en place des verrous entre fils d’exécution (value semantics).
Sucre syntaxique
[N3928] static_assert(expr)
avec un seul paramètre
(sans le second paramètre message
)
C++17 permet d’écrire static_assert(condition)
avec un seul paramètre. Avant, seule la fonction static_assert(condition, message)
était disponible avec le second paramètre message
obligatoire.
// Les static_assert avec un message vide étaient courants
static_assert(sizeof(int) == 4, "");
// L'usage (mauvaise pratique?) a influencé C++17
static_assert(sizeof(int) == 4);
Pour l’anecdote, cette fonctionnalité aurait bien pu s’appeler constexpr_assert()
car constexpr
exprime qui est évalué lors de la compilation. Donc constexpr
est plus précis que static
dans le nom constexpr_assert()
. La fonctionnalité static_if
s’est bien fait renommée constexpr_if
(voir l’historique de P0292).
Soulignons que les mots clés constexpr
et static_assert()
permettent au C++ de réaliser l’adage “Compilé c’est testé, linké c’est livré” comme illustré par le comic strip ci-dessous.
[N4230] namespace aa::bb
imbriqué
En C++17, écrire namespace
aa::bb { }
correspond à namespace aa { namespace bb { } }
.
// Avant C++17
namespace aa {
namespace bb {
}
}
// Avec C++17
namespace aa::bb {
}
[N4295] Expression dépliable
Les fonctions et classes variadiques peuvent réaliser des expressions dépliables, exemple :
template<typename ...Types>
auto somme (Types... valeurs)
{ return (valeurs + ...); }
template<bool ...Valeurs>
auto un_seul()
{ return (... || Valeurs); }
int main()
{
if (un_seul<false,true,false>())
return somme(1,2,3,4);
}
[P0036] Révision de la TS précédente N4295
Cette TS limite les ambitions de la TS sur les expressions dépliables.
std::vector v1 {1, 2, 3};
std::vector v2 {2, 2, 2};
// La fonction mixe
// mélange les vecteurs
// et retourne un vecteur
auto v3 = mixe(v1, v2);
template<typename ...Types>
auto fait_un_truc_magique (Types... x)
{ return mixe(x + ...); }
// Oups la fonction a été appelée sans paramètres
auto vec = fait_un_truc_magique();
// D'après N4295, la ligne ci-dessus revient à faire
auto vec = mixe(0);
D’après la TS N4295, le compilateur aurait dû interpréter l’absence de paramètre dans mixe(x + ...)
comme mixe(0)
. Cette TS interdit l’absence de paramètre pour les expressions dépliables utilisant + * & |
. Attention, cette TS n’a pas été suffisamment vérifiée…
[P0017] Héritage des agrégats d’initialisation
L’agrégat d’initialisation c’est le nom barbare d’une technique bien connue des développeurs C et C++ :
struct Personne
{
std::string nom;
int age;
};
// Initialisation avec un agrégat
Personne a{"Alice", 42};
Ce TS permet d’utiliser également les agrégats d’initialisation avec l’héritage :
struct Poste
{
std::string metier;
int anciente;
Poste() { metier = "non spécifié"; }
};
struct Collegue : Personne, Poste
{
bool teletravail = false;
};
Collegue b{ {"Bob", 30}, {"Développeur", 10}, true };
Collegue c{ {"Cloé", 40}, {"Manager", 15} };
Collegue d{ {"Didier", 50}, {} }; // metier = "non spécifié", anciennete = 0
Collegue e{ {"Élodie"}, {"Big Boss"}, false };
Attention, ces exemples ci-dessus n’ont pas pu être vérifiés faute d’implémentation de cette fonctionnalité dans Clang-3.9 et GCC-7.
[P0091] Déduction des arguments template
du constructeur
Le compilateur peut déduire les arguments template
des fonctions. Exemple:
template <class T>
T foo(T v) { return v++; }
// Déduit que le type T est int
auto v = foo(0);
L’idée est d’étendre cette déduction aux classes, via les arguments des constructeurs. Attention, les compilateurs GCC-7 et Clang-3.9 ne compilent pas (encore?) le code source ci-dessous. Ce code est peut-être erroné…
// Avec les fonctions d'aide
auto a = std::make_array(1,2,3);
// Avant C++17 et Avec C++17
std::array<int,3> avant{1,2,3};
std::array avec{1,2,3};
std::pair<int,char> p_avant(4,'L');
std::pair p_avec(4,'L');
auto t_avant = std::make_tuple("voiture",4,'L');
std::tuple t_avec("voiture",4,'L');
Déjà qu’une partie des fonctions d’aide make_***()
ne sont plus très utiles avec l’arrivée des initializer-list (en), comme pour make_pair
. Mais en plus, avec la déduction des paramètres template
au niveau du constructeur, il n’y a plus beaucoup de fonctions d’aide make_***()
qui vont rester indispensables !
[P0127] Déclaration des paramètres template<auto>
Le template<auto>
permet de remplacer MaClasse<int,42>
par un élégant MaClasse<42>
. Apparemment, ce n’est pas encore supporté par Clang-3.9 et GCC-7.0.
template <typename Type, Type Constante>
struct Cpp14
{
auto foo() const {
auto x = Constante;
return ++x;
}
};
template <auto Constante>
struct Cpp17
{
auto foo() const {
auto x = Constante;
return ++x;
}
};
int main()
{
const auto x = 42;
Cpp14<decltype(x), x> cpp14;
Cpp17<x> cpp17;
return cpp14.foo() + cpp17.foo();
}
Cette nouveauté permet de simplifier l’écriture du std::integral_constant
:
template <auto v>
struct integral_constant
{
using type = integral_constant;
using value_type = decltype(v);
static constexpr value_type value = v;
constexpr operator value_type() const noexcept { return value; }
constexpr value_type operator()() const noexcept { return value; }
};
Et par conséquent, simplifier un peu la méta-programmation :
template <auto x, auto y>
constexpr auto
operator+(integral_constant<x>, integral_constant<y>)
{ return integral_constant<x + y>{}; }
template <auto x, auto y>
constexpr auto
operator*(integral_constant<x>, integral_constant<y>)
{ return integral_constant<x * y>{}; }
integral_constant<1> un;
integral_constant<2> deux;
integral_constant<3> trois;
auto resultat = deux * trois + un;
static_assert( resultat.value == integral_constant<7>::value );
[P0217] Attaches structurées (Structured bindings)
La décomposition du retour de fonction est limitée aux std::tuple
, aux tableaux (comme std::array
) et aux structures plates (comme std::pair
).
struct A
{
int a;
bool b;
char c;
};
A foo() {
return A{42, true, 'c'};
}
auto [ x, y ] = foo();
// Attention, non testé...
assert(x == 42);
assert(y == true);
static_assert(std::is_same_v<decltype(x),decltype(A::a)>);
static_assert(std::is_same_v<decltype(y),decltype(A::b)>);
Quand la fonction retourne un std::tuple
(comme le std::pair
) il était déjà possible d’utiliser std::tie
. Mais la décomposition est plus commode (sauf pour le std::ignore
).
#include <set>
#include <tuple>
int main()
{
std::set<int> entiers;
// Avant C++17
std::set<int>::iterator iter;
bool inserted;
std::tie(iter, inserted) = entiers.insert(42);
// Avec C++17
auto [ it, is_inserted ] = entiers.insert(42);
}
[P0305] if(init;condition)
et switch(init;condition){}
Ce TS introduit les instructions de sélection avec initialiseur.
En concret, c’est l’ajout du terme initialisation;
dans les instructions :
if( initialisation; condition )
etswitch( initialisation; condition )
Notons que cela ressemble à for( initialisation; condition; incrémentation )
.
L’expression if(MonType v = truc())
peut donc être remplacée par if(MonType v = truc(); v)
.
// Version verbeuse
{
auto paire = map.insert({k,v});
auto it = paire.first;
bool inserted = paire.second;
if (inserted)
{
foo(it);
return iterateur;
}
} // <- bloc pour limiter la portée des variables
// Version C++17
if (auto [it, inserted] = map.insert({k,v}); inserted)
{
foo(it);
return iterateur;
}
Corrections
[N3922] Déduction auto x{42};
comme int x{42};
Ce TS comble un trou sur les règles de déduction pour auto
à partir des {listes d’initialisation}.
auto a {1}; //ok => int a{1};
auto b = {1}; //ok => std::initializer_list<int> b = {1};
auto c {1, 2}; //KO car plus d'un élément
auto d = {1, 2}; //ok => std::initializer_list<int> d = {1, 2};
auto e {1, 2.0}; //KO car plus d'un élément
auto f = {1, 2.0}; //KO car pas le même type (int et double)
Exercice : Chercher la différence entre l’image ci-dessous et celle au tout début de la dépêche.
[N4051] Autoriser typename
pour les paramètres template template
Avant C++17, seule la classe Cpp98
était conforme au standard :
template<template<typename> class T> class Cpp98;
template<template<typename> typename T> class Cpp17;
Utilité des paramètres template template
Mais à quoi cela sert les paramètres template template
?
Cela sert, par exemple, à obtenir les paramètres template
d’un conteneur :
template< template <typename, typename> typename Container
, typename T
, typename Allocator >
auto convert_to_vector (const Container<T, Allocator>& container)
{
std::vector<T, Allocator> v;
v.reserve(container.size());
for (const auto& e : container) v.push_back(e);
return v;
}
C’est juste un exemple, la version suivante est plus pertinente :
template<typename Container>
auto convert_to_vector (const Container& container)
{
std::vector<typename Container::value_type, typename Container::allocator_type> v;
v.reserve(container.size());
for (const auto& e : container) v.push_back(e);
return v;
}
Utile pour vérifier si le paramètre template
est correct
Un usage courant est de détecter des incohérences dès l’instanciation d’une class template
. L’exemple suivant montre comment s’assurer que l’Allocator
et la Factory
de WidgetManager
gèrent bien le type Widget
. L’exemple compile pour VC++15. La version pour GCC6 n’a pas besoin du mot-clé typename
dans la déclaration des types alias.
struct Widget
{};
template <typename T>
struct MyAllocator
{};
template <typename T, template<typename> class T_Allocator>
struct MyFactory
{};
template
< typename T
, template <typename> class Allocator
, template <typename, template<typename> class> typename Factory
>
struct WidgetManager
{
using type = T;
using allocator_type = typename Allocator<type>;
using factory_type = typename Factory<type, typename Allocator>;
}; //GCC6: Pas besoin de ^^^^^^^^ ni de ^^^^^^^^
int main()
{
WidgetManager<MonWidget, MonAllocateur, MaFabrique> MonWM;
}
D’autres idées ?
Chère lectrice, cher lecteur LinuxFr.org,
Tu as peut-être déjà utilisé les paramètres template template
?
Ou tu as peut-être de meilleurs idées sur l’utilité d’une telle fonctionnalité ?
Alors partage dans les commentaires tes exemples ;-)
Et le top du top serait un exemple de paramètres template template
variadiques !
Un truc du genre :
template< template<typename,typename,typename...> typename Truc
, typename Premier >
, typename Deuxieme >
, typename... Reste >
auto machin( const Truc<Premier, Deuxieme, Reste...>& t1
, const Truc<Deuxieme, Premier, Reste...>& t2)
{
Truc<Reste...> t3 (t1, t2);
return t1 + t2 - t3;
}
Anecdote
À l’époque lointaine où les template
avaient été introduites au C++, seul le mot-clé class
permettait d’indiquer que le paramètre template
est un type (le mot-clé typename
n’existait pas encore). Donc, pour un paramètre template
, le mot-clé class
désigne tous les types, dont struct
, int
… (pas seulement les types class
). Dans l’exemple ci-dessous, class T
signifie que T
est un type, n’importe lequel :
template <class T>
T foo(T);
int v = foo(42);
Mais les compilateurs C++ avaient du mal à déduire la nature des certaines instructions :
template <class T>
void foo()
{
T::fonction(); // Est-ce objet de type T::fonction ?
T::type(); // Est-ce un appel de fonction ?
T::type * v; // Est-ce une multiplication ?
T::constante** v; // Est-ce une déclaration ?
}
Alors le mot-clef typename
a été ajouté, exemple :
template <class T>
void foo()
{
T::fonction(); // Appel de fonction
typename T::type(); // Objet temporaire
typename T::type* v; // Déclaration d'une variable
T::constante * *v; // Multiplication
}
Puis, le mot-clef typename
a aussi remplacé class
pour le paramètre template
. Et c’est plus logique car ce n’est pas restreint aux seuls types déclarés avec le mot-clé class
. La rétrocompatibilité ayant été conservée, les mots-clés typename
et class
sont interchangeables pour les paramètres template
.
template <class T> // ou <typename T>
void foo() { }
template <typename T> // ou <class T>
void bar() { foo<T>(); }
Et avec C++17, le cas du paramètre template template
peut enfin utiliser typename
.
C++ without class
Alors que le nom originel du C++ était C with class
, nous pourrions dire que le C++17 est, par conséquent, le C++ without class
. C’est à dire, sans avoir besoin du mot-clé class
. En effet, class
est maintenant remplaçable partout soit par typename
, soit par struct
moyennant quelques changements :
template <class T, template<class> C>
class AvecClass : C<T>
{
int v;
};
template <typename T, template<typename> C>
struct SansClass : private C<T>
{
private:
int v;
};
Alors, qu’en penses-tu ?
C++ without class
c’est plus² la classe ?
[N4261] Conversion des tableaux de pointeurs
Ce TS complète la section 4.4 (conversion de qualification) sur la conversion d’un tableau de pointeurs constants/volatiles vers un autre type similaire.
double * tableau_2d[2][3];
double * (*a)[3] = tableau_2d;
double * const (*b)[3] = a;
double const* const (*c)[3] = b; // OK C++17
[N4267] Littéral de caractère UTF-8 u8
En programmation, un littéral est un préfixe ou un suffixe qui indique le type d’une constante (i.e. d’une valeur codée en dur). Ce TS corrige l’absence du littéral u8
pour les caractères déjà disponibles pour les chaînes de caractères. Nous avons maintenant quatre littéraux de caractères :
#include <type_traits> // std::is_same_v (pas encore dispo pour Clang-3.9)
// Pas de littéral
const auto narrow = 'a'; // char
static_assert(std::is_same_v<decltype(narrow), const char>);
// Nouveau littéral pour C++17
const auto utf8 = u8'e'; // char
static_assert(std::is_same_v<decltype(utf8), const char>);
// Littéraux déjà disponibles en C++ et C
const auto ucs2 = u'î'; // char16_t
const auto ucs4 = U'ö'; // char32_t
const auto wide = L'ù'; // wchar_t
static_assert(std::is_same_v<decltype(ucs2), const char16_t>);
static_assert(std::is_same_v<decltype(ucs4), const char32_t>);
static_assert(std::is_same_v<decltype(wide), const wchar_t>);
Notons que ce littéral u8
est (pour le moment?) absent des caractères en C comme le montre ce tableau récapitulatif :
Nom | Préfixe | Type | Chaîne de caractères | Caractère |
---|---|---|---|---|
Wide | L |
wchar_t |
C++98 et C89/C90 | C++98 et C89/C90 |
UTF-8 | u8 |
char |
C++11 et C11 | C++17 seulement |
UTF-16 | u |
char16_t |
C++11 et C11 | C++11 et C11 |
UTF-32 | U |
char32_t |
C++11 et C11 | C++11 et C11 |
Ce littéral n’avait pas été introduit auparavant car il peut induire en erreur. En effet, un caractère u8
ne peut contenir tous les codes UTF-8, seulement ceux qui peuvent être contenus dans un char
(sauf du char
représenté en 32 bits). En fait, le littéral u8
sert à représenter n’importe quel char
(dans le sens octet) d’une chaîne de caractère u8
qu’il soit un code UTF-8 valide ou pas.
const char* s = u8"aéîöù"; // Correct en C++11 et C11
const char c = u8'a'; // Correct en C++17
const char t[] = {u8'a',u8'e',u8'i'};// Correct en C++17
const char ko = u8'é'; // 'é' dépasse la capacité de stockage du char (1 octet)
const auto Ko = u8'é'; // Exactement le même problème, auto ne change rien
const char ok = u8'\xFF'; // Correct en C++17 (ce n'est pas un code UTF-8 valide)
const char a[] = u8"\xFF"; // Correct en C++11 et C11 (code UTF-8 invalide)
const char b[] = {u8'\xFF',u8'\x04'};// Correct en C++17 (deux octets pour un code UTF-8 valide)
L’exemple ci-dessus est intéressant car GCC-6 et GCC-7 affichent des avertissements (warning
) pour les variables [kK]o
alors que Clang-3.6 à Clang-3.9 produisent des erreurs : GCC considère que ce code est conforme au standard C++17 (car pas d’erreur) alors que Clang non.
[N4268] Autoriser l’évaluation constante pour les arguments template
Cette TS harmonise les évaluations constantes pour les arguments template
n’étant pas un type. Les arguments de type pointeur, référence et pointeur-vers-membre acceptent davantage d’expressions constantes. C’est un oubli de C++11 qui avait pourtant étendu la notion d’expression constante. La table suivante résume les changements.
Type | C++14 | C++17 |
---|---|---|
Pointeur | &variable , tableau, fonction référant un objet statique ou nullptr |
évaluation d’une adresse constante d’un objet complet statique ou d’une fonction, ou nullptr |
Référence | objet ou fonction référant un objet statique | évaluation d’un glvalue constant référant un objet complet statique ou d’une fonction |
Pointeur-vers-membre | &S::statique ou nullptr |
toutes expressions constantes |
Intégral bool char int … |
toutes expressions constantes | pareil |
enum |
toutes expressions constantes | pareil |
nullptr_t |
toutes expressions constantes | pareil |
Le code source suivant permet de vérifier que cette fonctionnalité est déjà prise en compte par GCC-6 et Clang-3.6 :
// Cette classe permet de tester
// les expressions constantes
template<int* ExpressionConstante>
class Test
{ };
// La fonction constexpr getPtr() retourne
// un pointeur vers un objet statique
// (static storage duration object)
int entier = 42;
constexpr int* getPtr() {return &entier;}
constexpr int* getNullptr() {return nullptr;}
Test<&entier> ok_entier;
Test<getPtr()> ok_Cxx17; //KO C++14
Test<getNullptr()> ok_nullptr;
// L'expression &obj.statique est un
// pointeur-vers-membre d'un objet statique
struct Str
{ int membre; static int statique; };
Str obj;
Test<&Str::membre> ko_ptr_non_statique;
Test<&Str::statique> ok_ptr_statique;
Test<&obj.statique> ok_cxx17; //KO C++14
// Le pointeur vers un élément de tableau
// statique ne semble pas être supporté
int tableau[5];
Test<&tableau[2]> ko_adresse_element;
[N4285] Réécriture des paragraphes concernant les exceptions
Ce TS a le mérite d’identifier des paragraphes qui ne sont pas clairs et de proposer une reformulation.
Profitons de ce TS pour faire l’analogie entre le standard C++ et la loi en France. Pour savoir comment la loi s’applique, il faut d’abord comprendre les textes votés par les députés, puis prendre en compte les amendements et enfin vérifier la jurisprudence sur le terrain. Et bien nous avons une ressemblance avec le standard C++ : la norme ISO/IEC correspond à la loi (la théorie), les Defect Reports (rapports d’anomalie) et l’implémentation des compilateurs est la jurisprudence.
[P0012] noexcept
devient un type système
Ce TS intègre les spécifications d’exception dans le type système, c’est à dire que noexcept
devient un type système afin de pouvoir distinguer les types de pointeur de fonction noexcept
des autres.
Ainsi C++17 peut interdire la conversion de pointeurs de fonction throw()
vers ceux de fonctions noexcept
, mais l’inverse est toujours possible. Le code suivant est valide en C++14, mais ne compile plus avec C++17 :
void (*p)() throw(int);
void (**pp)() noexcept = &p; //Erreur C++17
struct S
{
typedef void (*p)();
operator p();
};
void (*q)() noexcept = S(); //Erreur C++17
template<class T>
void f(T*, T*) {}
void g1() noexcept {}
void g2() {}
f(g1, g2); //Erreur C++17
[P0035] Allocation mémoire dynamique des données
C++11 a permis de préciser l’alignement mémoire d’une structure avec le mot clef alignas
class alignas(16) maclasse {
float f[4];
};
Mais rien n’avait été précisé dans le cas d’une allocation dynamique
maclasse *p = new maclasse[5];
Dans ce cas l’alignement mémoire de la zone allouée restait au choix du compilateur.
Avec C++17 des surcharges des opérateurs new
et delete
permettent de préciser l’alignement.
new maclasse[5]
est converti soit en :
operator new[](sizeof(maclasse)*5+x)
operator new[](sizeof(maclasse)*5+x, std::align_val_t(alignof(maclasse)))
nouvelle surcharge)
Si std::align_val_t(alignof(maclasse))
est supérieur à __STDCPP_DEFAULT_NEW_ALIGNMENT__
, c’est la nouvelle surcharge de new
qui sera utilisée.
[P0136] Héritage des constructeurs
Certaines règles d’héritage des constructeurs via using
ont été modifiées dans un souci de cohérence et de simplification. Concrètement, plusieurs cas d’héritages précédemment interdits sont maintenant autorisés.
Le Substitution Failure Is Not An Error (SFINAE) sur les constructeurs hérités fonctionne de manière fiable :
struct A {
template<typename T> A(T, typename enable_if<sizeof(T) == 4, int>::type = 0);
};
struct B : A {
using A::A; // déclare les constructeurs suivants :
// #1: B(T)
// #2: B(T, typename enable_if<...>::type)
// car maintenant A::A(T, typename enable_if<...>::type = 0)
// est un constructeur visible depuis B
B();
};
B b(123ull); // Autorisé en C++17
Paramètres variables :
struct A { A(const char *, ...); };
struct B : A { using A::A; };
B b("%d %d", 1, 2); // Maintenant accepté dans toutes les implémentations
Les droits d’accès sont maintenant identiques à ceux de la classe de base :
class A {
public:
template<typename T> A(T, typename T::type);
private:
A(int);
friend void f();
};
class B {
typedef int type;
friend class A;
};
struct C : A {
using A::A;
};
void f() {
A a1(B(), 0); // accepté (pas de changement)
C c1(B(), 0); // précédemment refusé (le constructeur implicite de C ne pouvait pas accéder à B::type), maintenant accepté
A a2(42); // accepté (pas de changement), f est un `friend`)
C c2(42); // précédemment refusé,( (le constructeur implicite de C ne pouvait pas accéder au constructeur de A), maintenant accepté
}
C c3(123); // reste refusé, pas d'accès à A(int)
En présence d’arguments par défaut, le masquage des constructeurs fonctionne maintenant de la même manière que pour les autres membres hérités via using
.
struct A {
A(int a, int b = 0);
void f(int a, int b = 0);
};
struct B : A {
B(int a); using A::A;
void f(int a); using A::f;
};
struct C : A {
C(int a, int b = 0); using A::A;
void f(int a, int b = 0); using A::f;
};
B b(0); // était accepté, maintenant ambigu.
b.f(0); // ambigu(pas de changement)
C c(0); // était ambigu, maintenant accepté
c.f(0); // accepté (pas de changements)
[P0138] Liste d’initialisation pour enum
Voici comment créer un type byte
en C++98 :
typedef unsigned char byte;
byte b1 = 42; // OK C++98
byte b2(42); // OK C++98
byte b3{42}; // OK C++11
Une autre possibilité mi-C++98, mi-C++11 :
struct byte { unsigned char v; };
//byte b1 = 42; // Erreur
//byte b2(42); // Erreur
byte b3{42}; // OK C++11
Avec les possibilités du C++11 :
using byte = unsigned char;
byte b1 = 42; // OK C++11
byte b2(42); // OK C++11
byte b3{42}; // OK C++11
byte b4 = {42}; // OK C++11
Et grâce à ce TS intégré à C++17 :
enum byte : unsigned char {};
//byte b1 = 42; // Erreur
//byte b2(42); // Erreur
byte b3{42}; // OK C++17
//byte b4 = {42}; // Erreur
Un exemple résumant ce qui est permis ou pas :
// Peut être vide ou contenir des énumérés
enum E : int { X, Y, Z };
int v = 42;
E e1{42}; // OK
E e2{v}; // OK
v = e1 + e2; // OK
E e = {0}; // Erreur
E e = E{42}; // OK
const E& e{42}; // OK
const E& e = {42}; // Erreur
E* p = new E{42}; // OK
E f() { return {0}; } // Erreur
E g() { return E{0}; } // OK
auto h(E e) { return e; } // OK
h({0}); // Erreur
h(E{0}); // OK
struct A {
E e{0}; // OK
E e = {0}; // Erreur
A() : e{0} {} // OK
};
struct B { E e; };
B b = { {42} }; // Erreur
B b = { E{42} }; // OK
[P0184] Généralisation des boucles pour gérer les Intervalles (Ranges)
Cette petite TS permet aux boucles for
“each” de gérer des conteneurs ayant des begin()
et end()
retournant des types différents mais comparables.
Cette correction est nécessaire à l’implémentation des intervalles. Cela permet également de supporter d’avantage de valeurs sentinelles.
[P0296] Forward progress guarantees (FPG) et [P0299] FPGs for parallel algorithms
Ces deux TS concernent le verrouillage des fils d’exécution (thread lock) et des situations de compétition (race condition) afin de limiter les cas de boucle infinie.
Compatibilité avec le langage C
[P0063] C++17 se réfère à C11 au lieu de C99
C++17 est maintenant basé sur C11 au lieu de C99 + C Unicode TR.
Ajout des en-têtes (headers) <stdalign.h>
et <uchar.h>
. On ignore les en-têtes C11 <stdatomic.h>
, <threads.h>
et <stdnoreturn.h>
. Dépréciation des en-têtes <ccomplex>
, <ctgmath>
, <cstdalign>
, <cstdbool>
, <complex.h>
, <stdalign.h>
, <stdbool.h>
et <tgmath.h>
.
Donc, C++17 se base sur une partie du C11. Par exemple, la gestion du multitâche du C n’est pas prise en compte dans le standard (donc c’est optionnel). Pour la première fois, le C++ n’est plus rétrocompatible — du moins une partie — avec les en-têtes du C (le C était un sous-ensemble du C++).
Finalement, peu de changements. Par exemple, la fonction aligned_alloc()
avait déjà été intégrée avec C++11.
Suppression
[N4086] Trigraphes
Il était une fois, un clavier qui ne permettait pas d’insérer tous les caractères nécessaires aux langages de programmation. Alors les digraphes, puis les trigraphes furent inventés afin de pallier cette limitation. C’était également le cas des premiers claviers français qui n’avaient pas tous les caractères des claviers des États-Unis, comme la barre oblique inversée \\ de ce clavier AZERTY.
// Avec digraphes et trigraphes
??=include <iostream>
int main(int argc, char *argv<::>) ??<
const char hello_world ??(??) = "Hello world !??/0";
std::cout << hello_world << std::endl;
??>
// Sans digraphe/trigraphe
#include <iostream>
int main(int argc, char *argv[]) {
const char hello_world[] = "Hello world !";
std::cout << hello_world << std::endl;
}
Certaines entreprises maintiennent du très vieux code C/C++ contenant des digraphes et trigraphes. Ces digraphes/trigraphes pourraient être remplacés par les caractères correspondants avec un script. Mais ces entreprises préfèrent les garder dans leur code source, maintenir cette complexité dans les compilateurs et entretenir la surprise quand les développeurs utilisent certaines suites de caractères.
Question piège : Qu’affiche ce code source en C++98, C++14 et C++17 ?
#include <iostream>
int main()
{
std::cout // La question ?????/
<< "L'univers, la vie et le reste\n"
<< "Indiquer la date au format ??/??/??\n";
}
Réponse
-
En C++98 et C++14, même comportement
$ g++ -std=c++14 main.cpp && ./a.out Indiquer la date au format \??
-
En C++17, l’affichage est très différent
$ g++ -std=c++1z -w main.cpp && ./a.out L'univers, la vie et le reste Indiquer la date au format ??/??/??
Anecdote : Les trigraphes auraient pu devenir obsolètes dès C++11 (proposé en 2009). Mais, quelques membres du comité de normalisation du C++, dont IBM et Bloomberg, avaient réussi à ne pas les rendre obsolètes. Pour C++17, les membres ont finalement voté la suppression pure et simple sans étape intermédiaire. IBM a même tenté une dernière tentative pour conserver les trigraphes mais sans succès.
Cette suppression simplifie grandement les compilateurs car les passes des digraphes et trigraphes ne s’effectuent pas au même moment.
Alors que tripgraphe signifie comme son nom l’indique trois caractères, digraphe en C++ est un abus de langage car il intègre le digraphe %:%:
composé de 4 caractères. Voici ce que dit le standard :
The term “digraph” (token consisting of two characters) is not perfectly descriptive, since one of the alternative preprocessing-tokens is and of course several primary tokens contain two characters. Nonetheless, those alternative tokens that aren’t lexical keywords are colloquially known as “digraphs”.
Dans un sens plus large le standard parle de Alternative tokens gérant de la même façon les digraphes et quelques autres mots-clés. Voici ce qui en reste pour C++17 :
Token | Correspondance |
---|---|
<% |
{ |
%> |
} |
<: |
[ |
:> |
] |
%: |
# |
%:%: |
## |
and |
&& |
bitor |
| |
or |
|| |
xor |
^ |
compl |
~ |
bitand |
& |
and_eq |
&= |
or_eq |
|= |
xor_eq |
^= |
not |
! |
not_eq |
!= |
[P0001] Mot-clé register
Historiquement, le mot-clé register
force l’utilisation d’un registre du processeur. Cela permettait de gagner en performance en indiquant au compilateur quelles variables à garder dans un registre (à l’époque les compilateurs n’étaient pas très futés).
// ERREUR Interdit sur les variables globales
register int g = 0;
int main()
{
register int r = 0; // OK
// ERREUR Une variable register n'a pas d'adresse
// Mais GCC et Clang le tolère.
register int *pointeur = &r;
register int tableau[1]; // Comportement indéfini
}
Le mot-clé register
est déprécié depuis C++11. À l’époque, les contraintes de ce mot-clé (pas de pointeur…) ont été conservées pour la compatibilité avec le C, en particulier avec les arguments des fonctions. Pourtant, son usage n’est pas pertinent en C++ : redondant avec d’autres fonctionnalités et ses restrictions ne peuvent être facilement transcrites en C++. Plutôt que d’essayer de résoudre les différences avec le C, le standard fait de register
un mot-clé réservé non utilisé. Espérons qu’un usage futur lui soit trouvé…
[P0002] Incrémentation des booléens
Dans les temps anciens, le type bool
n’existait pas. Les entiers étaient utilisés pour cet usage avec :
#define BOOL int
#define FALSE 0
#define TRUE !FALSE // ou !0 ou 1, selon les implémentations
C’est à dire zéro pour faux et toutes les autres valeurs pour vrai. Voir stdbool.h.
La création du type bool
avec le C++ avait nécessité de garder une comptabilité avec le vieux code : l’incrémentation avait été autorisée mais pas la décrémentation.
bool b = aimes_tu_cpp17();
--b; // Erreur depuis C++98
++b; // Erreur depuis C++17
Fonctionnalités majeures non intégrées
Par contre, au moment de la clôture du périmètre fonctionnel C++17 (juin 2016), les fonctionnalités suivantes n’ont pas été considérées comme suffisamment mures :
- Concepts avec les mot-sclés
concept
etrequires
(voir aussi la version en anglais) ; - Modules, par exemple
import std.string;
à la place de#include <string>
(A Module System for C++) ; - Syntaxe d’appel uniforme (Uniform call syntax) ;
- Réflexion (Static Reflection) avec le mot-clé
reflexpr
.
Exemple final
Donc en C++17 nous pourrons écrire :
#include <array>
struct Truc
{
bool b = false;
float f = 0xA.Bp3f; // hexa
};
int main (int argc,
[[maybe_unused]] char* argv[])
{
// Déduction array<int,4>
std::array tableau {1,2,3,argc};
// lambda constexpr
auto f = [](){ return Truc(); };
if constexpr (auto [x,y]=f(); x)
return y + tableau[1] * argc;
else
return y - tableau[2] * argc;
}
Alors, chères lectrices et chers lecteurs de LinuxFr.org ? Séduits ? Conquis ? Impatients de coder en C++17 ? La tentation est grande d’épater ses collègues avec du code qu’ils ne comprennent plus… non ?
Deux concours
Cette dépêche propose deux concours :
- Le premier sur les jeux de mots du littéral hexadécimal des virgules flottantes ;
- Le second sur du code C++11/14/17 mais qui ne ressemble plus au traditionnel C++98 que nous avons eu l’habitude de côtoyer pendant une vingtaine d’années.
Pour ce second concours, proposez dans les commentaires un code source utilisant les différentes nouveautés du C++17 (et aussi C++11/14). Un développeur étant resté au bon vieux C++ du siècle dernier devrait croire que ce n’est pas du C++. Objectif : le code le plus différent du C++98. Néanmoins, le code source doit être facilement compréhensible par un développeur habitué aux récentes versions du C++, ce n’est pas un concours d’offuscation. Le code source doit être sous licence libre, pensez à bien faire figurer la licence de votre choix.
Les meilleures réponses seront sélectionnées parmi celles qui auront le plus de points « pertinents » et le moins de points « inutiles ». Le résultat se fera sur la dépêche suivante. Les vainqueurs recevront des petits cadeaux (goodies, autocollants…) collectés sur les différents stands au Paris Open Source Summit les 16 et 17 novembre 2016. Et peut-être même un livre à choisir parmi ceux des éditions Eyrolles et ENI. Possibilité d’envoyer les récompenses par courrier électronique postal.
Appel à participation
La précédente dépêche a reçu 227 commentaires, soit un volume dix fois supérieur à la dépêche elle-même. Tous ces commentaires cachent tout de même quelques joyeux trolls velus !
Quand on pense à toute cette énergie dépensée et toutes ces heures consacrées à rédiger ces 227 commentaires ! Avec le recul nous aurions pu concentrer tout cet investissement dans une dépêche collaborative du style « Aujourd’hui, est-il pertinent de choisir le C++ pour une nouvelle application ? ».
Mais il n’est jamais trop tard ! Aussi nous proposons vous de rédiger la dépêche « Faut-il continuer à apprendre le C++ ? » Les nombreux commentaires de la dépêche précédente méritent d’y être copiés. Malheureusement, ceux-ci sont rarement sous licence compatible CC-BY-SA-4.0. Ceci est donc un appel à tous leurs auteurs de les copier dans cette dépêche afin de la nourrir. Ainsi, nous pourrons les structurer et proposer des réponses concises, claires et utiles à tous.
Merci et à vos claviers ! ;-)
Réutilisation
Le texte de cette dépêche est protégé par le droit d’auteur la gauche d’auteur et réutilisable sous licence CC BY-SA 4.0. Les images utilisées sont aussi sous licence libre (cliquer sur l’image pour plus de détails).
Donc n’hésitez pas à réutiliser ce contenu libre pour créer, par exemple, des supports de formation, des présentations (Meetups), des publications sur d’autres blogs, des articles pour des magazines, et aussi un article C++17 sur Wikipédia dès que Wikipédia passera de la licence CC-BY-SA-3.0 à la CC-BY-SA-4.0 (le contenu de cette dépêche utilise la version la CC-BY-SA-4.0).
Les auteurs
Par respect de la licence, merci de créditer les auteurs :
- Les principaux auteurs sont Adrien Jeser et Oliver H ;
- Les nombreux autres contributeurs dont l’ancêtre de cette dépêche ou via le dépôt Git sont eggman, Yves Bourguignon, Storm, gorbal, palm123, khivapia, BAud, Segfault, Benoît Sibaud, Lucas, cracky, Martin Peres, RyDroid, olibre et Guillaume Dua.
Un immense merci à toutes ces personnes ayant rédigés bénévolement un article de très grande qualité. Merci aussi à Ziyue et Oliver H pour avoir dessiné spécialement pour cette dépêche l’écolière sauvée par le C++ et « compilé c’est testé » (tous deux sous licence CC BY-SA 3.0). Merci aux auteurs des autres illustrations.
Continuer à corriger et à améliorer ce document
Malgré tout le soin apporté, il reste certainement des oublis, des ambiguïtés, des fôtes… Bien que cette dépêche restera figée sur le site LinuxFr.org, il est possible de continuer à l’enrichir sur le dépôt Git du Groupe des Utilisateurs C++ Francophone (C++FRUG). C’est donc sur ce dépôt que se trouve les versions les plus à jour. Merci de nous aider à maintenir ce document à jour ;-)
La suite
Nous venons de découvrir les nombreuses évolutions au niveau du langage : le cœur du C++. La dépêche suivante nous emmènera au niveau de la bibliothèque standard.
Chère lectrice, cher lecteur LinuxFr.org. Tu souhaites apporter ta pierre à cet édifice ? Rejoins‐nous dans l’espace de rédaction collaborative sur LinuxFr.org (un compte est nécessaire pour y accéder).
À suivre…