I. Le problème▲
Nous allons commencer par un bug, tiré de la vie réelle. Il englobe trois fichiers :
2.
3.
4.
5.
#include
<string>
struct
Service
{
static
const
std::
string NAME;
}
;
2.
#include
"service.h"
const
std::
string Service::
NAME =
"SERVICE1"
;
2.
3.
4.
5.
6.
7.
#include
"service.h"
#include
<iostream>
const
std::
string MSG =
"service "
+
Service::
NAME +
" ready"
;
int
main()
{
std::
cout <<
MSG <<
std::
endl;
}
Question : que se passe-t-il si le programme est exécuté ?
Quand j’ai testé ce programme dans mon environnement, même si je ne m’attendais pas à avoir le résultat escompté, j’étais quand même surpris du résultat. Le programme s’exécute sans problème et donne en sortie :
service ready
Le programme touche ce qui est appelé le fiasco d’ordre d’initialisation statique : pendant que la variable globale B est en train d’être initialisée, nous avons besoin de lire la variable globale A, mais A peut être initialisée après B. J’ai touché le mauvais ordre d’initialisation clairement, mais je m’attendais à ce que le programme crashe ou donne en sortie des caractères aléatoires. Mais apparemment dans ma version de la bibliothèque standard, un std::
string rempli de zéros (rappelez-vous que toutes les variables globales sont statiquement initialisées avec des zéros) représente une chaîne de caractères valide et vide. Si mon application crashe sur l’initialisation, au moins, chacun a le retour que quelque chose ne va pas bien. Mais mon application donne l’impression qu’elle fonctionne bien et affiche quelque chose d’autre que ce que je voulais.
Ce problème en réalité s’est manifesté d’une façon très caractéristique : quand le développeur a testé l’application dans son environnement, l’application a fonctionné comme voulu. Mais lors de l’exécution dans un environnement proche de la production, elle aurait crashé au démarrage.
Commentaire du traducteur :
si on avait fait
#include
<string>
const
std::
string Name =
"SERVICE1"
;
Et inclus ce fichier dans main.cpp, il n’y aurait pas de bug.
II. Que pouvons-nous faire ?▲
Une fois la source du problème identifiée, il est facile de corriger cette occurrence du bug. Au lieu de la constante NAME, fournissez une fonction avec une variable statique automatique à l’intérieur :
struct
Service
{
static
const
std::
string &
NAME()
{
static
const
std::
string _name =
"SERVICE1"
;
return
_name;
}
}
;
Une variable statique automatique est presque comme une variable globale : une instance est partagée à travers tous les appels à la fonction la contenant, et est initialisée une seule fois : quand le contrôle atteint la déclaration pour la première fois. De cette façon, vous garantissez que la chaîne de caractères est initialisée avant d’être retournée.
Et maintenant, vous voudrez probablement changer la variable globale MSG aussi, pour vous prémunir de surprises futures :
const
std::
string &
MSG()
{
static
const
std::
string _msg =
"service "
+
Service::
NAME()+
" ready"
;
return
_msg;
}
int
main()
{
std::
cout <<
MSG <<
std::
endl;
}
Mais cette solution n’est pas l’idéal. Premièrement, vous avez probablement remarqué que dans la fonction main, j’ai oublié d’ajouter les parenthèses. Mais c’est du C++ valide : l’adresse d’une fonction est implicitement convertissable en un bool
.
Si vous suivez une bonne pratique de programmation, et compilez avec les warnings activés (idéalement traités comme des erreurs), le compilateur peut vous aider à détecter le problème avant que vous ne produisiez l’exécutable. Mon GCC utilisé avec -
Wall me retourne :
warning : l’adresse de ‘const string&MSG()’
sera toujours évaluée comme ‘true’ [-Waddress]
Quelqu’un pourra argumenter que nous pouvons toujours traiter n’importe quelle fonction comme un objet, mais ici la situation est spéciale : nous avons une constante encapsulée dans une fonction. Conceptuellement, nous la traitons comme une constante. Et c’est dans ce fait le second inconvénient : nous avons fourni une notation de fonction pour lire une valeur constante.
Plus d’objections arrivent. Nous initialisons notre constante sur la première utilisation plutôt qu’au démarrage du programme. Cela signifie un ralentissement inattendu (pour allouer de la mémoire) à un endroit où vous ne vouliez pas en avoir un. Rappelez-vous qu'en pratique les chaînes peuvent être un peu plus longues que 6 caractères et ne bénéficieront pas du Small String Optimization (SSO). De plus, savez-vous ce qu'il se passe quand la fonction est appelée pour la première fois de façon concurrente par deux threads ? Une synchronisation (lire : un ralentissement).
En outre, les ressources nécessaires pour stocker les variables chaînes de caractères globales sont maintenant libérées bien après que main ait terminé. Les profileurs qui détectent les fuites mémoire font souvent la confusion avec ceci et reportent des fuites mémoire là où il n’y en a aucune.
Et finalement, il y a quelque chose de malsain à gérer les ressources avant que main ne démarre. C++ vous autorise techniquement à exécuter un programme de boucles de messages en dehors de main. Deux exemples :
const
bool
_ =
run_message_loop(), true
;
int
main() {}
struct
Launcher
{
Launcher() {
std::
set_terminate(&
run_message_loop); }
~
Launcher() {
throw
"launch"
; }
}
_ {}
;
int
main() {}
Mais faire cela est mauvais. Gérer les ressources avant main, bien que cela ne soit pas arrogant, est dans le même esprit : cela dépossède main de son rôle de point d’entrée du programme.
III. Vers la solution idéale▲
C++11 offre un outil pour prévenir le fiasco d’ordre d’initialisation statique : la capacité de construire des constantes durant la compilation :
constexpr
double
X =
fun1(Y) +
fun2(Z);
Ce que sont les variables fun1, fun2, Y et Z n'a pas d'importance.. À moins que l’initialisation du dessus de X engendre une erreur du compilateur, vous avez la garantie que c’est initialisé statiquement, n’utilise aucune ressource de runtime, et est libre de fiasco d’ordre d’initialisation et de comportement indéfini. En d’autres mots, c’est vérifié au moment de la compilation si vous avez un problème d’initialisation.
Mais je ne peux pas initialiser un constexpr
std::
string avec ceci : ce n’est pas un type littéral. Il a besoin d’allouer de la mémoire : il ne sait pas comment le texte contenu va grossir en taille. Mais attendez, ceci est vrai pour une chaîne de caractères généralement. Dans notre cas, cependant, nous savons exactement combien de lettres nous voulons stocker dedans. En principe, à moins que nous ayons besoin d’entrée runtime de notre environnement, il serait possible de calculer tout au moment de la compilation et de le stocker comme constantes au moment de celle-ci.
C’est ce que le C fait sur certains points. Que se passe-t-il ici :
const
char
NAME[] =
"SERVICE1"
;
static_assert
(sizeof
(NAME) ==
8
+
1
, "***"
);
Ce code dit au compilateur de préparer un stockage statique pour le tableau. La taille du tableau est déduite du littéral chaîne. La taille du tableau peut être déterminée au moment de la compilation. C'est presque ce que nous voulons. Nous pouvons, à certains points, concaténer des littéraux au moment de la compilation :
const
char
MSG[] =
"service "
"SERVICE1"
" ready"
;
static_assert
(sizeof
(NAME) -
1
==
8
+
9
+
6
, "***"
);
Mais malheureusement, nous ne pouvons faire ceci :
const
char
NAME[]=
"SERVICE1"
;
const
char
MSG[] =
"service "
NAME " ready"
;
SI le langage ne supporte pas ceci directement, nous serions capables de le faire avec une bibliothèque : une bibliothèque pour concaténer les chaînes de caractères au moment de la compilation.
IV. Implémenter la concaténation de string au moment de la compilation▲
Une contrainte additionnelle avant de procéder. Nous sommes en train de résoudre un problème de la vie réelle d'un programme de la vie réelle. Nous sommes en 2017 (date de rédaction), C++17 est presque là, C++14 est là depuis des années ; mais dans beaucoup d’environnements (comme le mien), les gens utilisent toujours le C++11. On a besoin d’une solution applicable pour le C++11.
L’idée de base derrière la solution peut être illustrée par la signature de l’opérateur de concaténation suivant :
template
<
int
N1, int
N2>
constexpr
auto
operator
+
(static_string<
N1>
s1, static_string<
N2>
s2)
->
static_string<
N1 +
N2>
;
Où static_string<
N>
est comme un tableau construit avec la taille N, suivi statiquement comme partie du type. On peut utiliser de la métaprogrammation pour calculer la taille de la chaîne concaténée.
Puisque la taille de telles chaînes de caractères est au moment de la compilation comme elle peut seulement être, les valeurs sont « constexpr- like » : elles peuvent être constantes au moment de la compilation ou non, cela dépend de l’utilisation. Donc, nous ne sommes pas aussi ambitieux que Boost.Metaparse, qui peut générer différents types pour différentes valeurs au moment de la compilation :
// possible avec Boost.Metaparse
auto
b =
PARSE_("bool"
); // decltype(b) == bool
auto
c =
PARSE_("char"
); // decltype(c) == char
Notre bibliothèque sera plus modeste. Nous nous focaliserons sur notre objectif : initialiser statiquement une chaîne de caractères possiblement concaténée.
Nous démarrerons en implémentant une référence à un littéral chaîne de caractères « C like », qui a la taille incorporée dans le type, et qui est un type distinct reconnu par notre bibliothèque de concaténation :
2.
3.
4.
5.
6.
7.
template
<
int
N>
class
string_literal
{
const
char
(&
_lit) [N+
1
];
public
:
// …
}
;
C++ (comme C) a déjà un stockage désigné pour les littéraux chaînes. Nous ne les copierons pas ; nous utiliserons au lieu de ceci une référence à ce stockage. Rappelez-vous que la taille d’un littéral est toujours plus grande d’un que le nombre de caractères dû au zéro final.
Maintenant, nous avons besoin d’un constructeur :
constexpr
string_literal(const
char
(&
lit)[N +
1
])
:
_lit(lit)
{}
Avec ceci, nous pouvons écrire :
constexpr
string_literal<
4
>
NAME =
"ABCD"
;
Mais l’utilisation suivante non voulue est aussi autorisée :
constexpr
char
array []={
'1'
, '2'
, '3'
, '4'
, '5'
}
;
constexpr
string_literal<
4
>
NAME =
array;
Pour prévenir ceci, nous mettons dans le constructeur que le dernier char
est une valeur numérique zéro :
constexpr
string_literal(const
char
(&
lit)[N +
1
])
:
_lit((X_ASSERT(lit[N] ==
'
\0
'
), lit))
{}
Nous avons couvert dans le précédent article comment écrire la macro X-
ASSERT qui fonctionne comme assert en C au moment de l’exécution et prévient la compilation au moment de la compilation.
Nous avons encore à expliciter la taille du string_literal initialisé. Idéalement, nous voudrions la taille déduite de l’initialiseur comme c’est le cas avec les tableaux C. Mais ceci est impossible en C++11.
Mais comme déduire des parties du type est impossible en C++11, déduire le type entier fonctionne assez bien si nous ajoutons une fonction template :
constexpr
auto
NAME =
literal("ABCD"
);
Ce n’est pas l’idéal parce que nous déclarons une variable sans un type, ce qui parfois conduit à des surprises. Mais la taille du littéral peut être déduite maintenant si nous implémentons literal intelligemment :
2.
3.
4.
5.
6.
template
<
int
N_PLUS_1>
constexpr
auto
literal(const
char
(&
lit)[N_PLUS_1])
->
string_literal<
N_PLUS_1 -
1
>
{
return
string_literal<
N_PLUS_1 -
1
>
(lit);
}
Ceci fait à peu près la même chose que std::
make_pair, avec une exception : parce que nous voulons des littéraux de taille N+1, nous ne pouvons pas juste taper N+1 directement dans les paramètres de fonction :
2.
3.
4.
5.
6.
7.
// WRONG:
template
<
int
N>
constexpr
auto
literal(const
char
(&
lit)[N +
1
])
->
string_literal<
N>
{
return
string_literal<
N>
(lit);
}
Ceci à cause de la façon dont les règles de correspondances de paramètres patrons fonctionnent : vous voulez déduire un entier, celui-ci doit correspondre avec un nom directement, pas une expression arithmétique arbitraire.
Une des choses dont vous aurez besoin pour ce nouveau type est d’accéder au énième caractère de la chaîne.
Ceci est assez facile à implémenter :
2.
3.
4.
5.
6.
7.
8.
9.
10.
template
<
int
N>
class
string_literal
{
const
char
(&
_lit)[N+
1
];
public
:
constexpr
char
operator
[](int
i) const
{
return
X_ASSERT(i >=
0
&&
i <
N), _lit[i];
}
}
;
Le const
additionnel est correct mais redondant en C++11. Cependant en C++14, constexpr
sur une fonction n’implique pas const
comme discuté dans cet article.
Maintenant, nous serons capables de concaténer deux string_literal de tailles différentes. Mais que serait le type (le résultat) de la concaténation ? Nous ne pouvons pas utiliser le même type comme ce n’est pas une référence à un littéral existant déjà. Nous aurons besoin d’un nouveau type qui stockera le tableau de caractères :
template
<
int
N>
class
array_string
{
char
_array[N+
1
];
}
;
Nous stockerons aussi le zéro final. Ceci pour fournir la fonction c_str() et l’interface à des bibliothèques « C-like » qui fonctionnent avec les suites de caractères terminées par le zéro final.
Écrire l’opérateur de concaténation est facile : nous transférons l’appel au constructeur de array_string :
template
<
int
N1, int
N2>
constexpr
auto
operator
+
(const
string_literal<
N1>&
s1,
const
string_literal<
N2>&
s2)
->
array-
string<
N1 +
N2>
{
return
array_string<
N1 +
N2>
(s1, s2);
}
La partie difficile concerne l'implémentation du constructeur. En C++14, qui a des contraintes plus souples sur constexpr
, ce serait facile :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
//en C++14
template
<
int
N>
class
array_string
{
char
_array[N +
1
];
public
:
template
<
int
N1, REQUIRES(N1 <=
N)>
constexpr
array_string(const
string_literal<
N1>&
s1,
const
string_literal<
N -
N1>&
s2)
{
for
(int
i =
0
; i <
N1; ++
i)
_array[i] =
s1[i];
for
(int
i =
0
; i <
N -
N1; ++
i)
_array[N1 +
i] =
s2[i];
_array[N] =
'
\0
'
;
}
}
;
Ne soyez pas dérangés par cette macro REQUIRES, c’est juste un raccourci pour std::
enable_if :
# define REQUIRES(...) \
typename std::enable_if<(__VA_ARGS__), bool>::type = true
Mais cette implémentation ne fonctionnera pas en C++11. En C++11, il est requis que le corps du constructeur constexpr
soit vide : vous pouvez seulement initialiser dans la liste d’initialisation :
2.
3.
4.
5.
6.
//en C++11
template
<
int
N1, REQUIRES(N1 <=
N)>
constexpr
array_string(const
string_literal<
N1>&
s1,
const
string_literal<
N -
N1>&
s2)
:
_array{
/* WHAT? */
}
{}
Maintenant, pour achever la chose presque impossible, vous devez être familiers avec les packs de paramètres dans les patrons variadiques, et comment ils peuvent être étendus avec la syntaxe suivante :
template
<
typename
... Args>
void
f(Args &&
... args)
{
g(std::
forward<
Args>
(args)...);
}
Vous noterez que std::
forward<
Args(args)... est une sorte de patron. Quand la fonction f est appelée comme f(1
,'c'
), le patron est développé en :
g(std::
forward<
int
>
(args1), std::
forward<
char
>
(args2));
Les virgules sont ajoutées quand c’est nécessaire. Mais une suite d’int
peut être aussi un pack de paramètres :
template
<
int
... Is>
void
f()
{
std::
vector<
int
>
v {
Is...}
;
}
Et quand nous appelons f<
0
,1
,2
>
(), l’initialisation du vecteur est développée en :
std::
vector<
int
>
v {
0
,1
,2
}
;
Ceci nous rapproche de la solution. Mais dans notre cas, nous avons besoin de deux packs. SI nous les avions d’une manière ou d’une autre, nous pourrions initialiser notre tableau comme ceci :
2.
3.
4.
5.
6.
// Pas encore C++ (pas de PACk 1 ou PACK2)
template
<
int
N1, REQUIRES(N1 <=
N)>
constexpr
array_string(const
string_literal<
N1>&
s1,
const
string_literal<
N -
N1>&
s2)
:
_array{
s1[PACK1]..., s2[PACK2]..., '
\0
'
}
{}
En C++14, la bibliothèque standard fournit un outil dédié exactement dans ce but : injecter des packs de paramètres « int-like » là où vous en avez besoin : integer_sequence. Cela vient en deux parties :
template
<
typename
I, I... Is>
class
integer_sequence;
Cette partie est utilisée pour « recevoir » un pack (par exemple pour développer un patron). La seconde partie est utilisée pour générer de tels integer_sequence pour un récepteur :
template
<
typename
I, I N>
using
make_integer_sequence =
/*...*/
;
Ce patron alias génère des instances d’integer_sequence selon l’attente suivante :
make_integer_sequence<
int
, 0
>
==
integer_sequence<
int
>
;
make_integer_sequence<
int
, 1
>
==
integer_sequence<
int
, 0
>
;
make_integer_sequence<
int
, 2
>
==
integer_sequence<
int
, 0
, 1
>
;
make_integer_sequence<
int
, 3
>
==
integer_sequence<
int
, 0
, 1
, 2
>
;
À savoir que le N dans make_integer_sequence détermine la taille de la séquence d’entiers dans l’integer_sequence, et que la séquence a des nombres consécutifs commençant à 0.
Maintenant, j’ai dit que ceci est seulement valable en C++14, et nous résolvons le problème pour le C++11. Par chance, c’est assez facile d’implémenter un outil similaire, et parce que nous avons seulement besoin que cela fonctionne avec des int
(plutôt qu'un type intégral), notre outil peut être plus petit :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
// le type utilisé pour recevoir le pack
template
<
int
... I>
struct
sequence {}
;
// meta-function auxiliaire pour faire des sequence de taille (N+1)
// depuis une sequence de taille N
template
<
typename
T>
struct
append;
template
<
int
... I>
struct
append<
sequence<
I...>>
{
using
type =
sequence<
I..., sizeof
...(I)>
;
}
;
// implementation recursive de make_sequence
template
<
int
I>
struct
make_sequence_;
template
<
int
I>
using
make_sequence =
typename
make_sequence_<
I>
::
type;
template
<>
struct
make_sequence_<
0
>
// recursion end
{
using
type =
sequence<>
;
}
;
template
<
int
I>
struct
make_sequence_ : append<
make_sequence<
I -
1
>>
{
static_assert
(I >=
0
, "taille negative"
);
}
;
Ceci n’est pas l’implémentation la plus efficace, mais c’est pour l’illustration. Si vous ne comprenez pas ce qu’il se passe ici, ne vous inquiétez pas. L’important c’est que de telles séquences peuvent être implémentées en C++11 et que cela expose les propriétés désirées :
make_sequence<
0
>
==
sequence<>
;
make_sequence<
1
>
==
sequence<
0
>
;
make_sequence<
2
>
==
sequence<
0
, 1
>
;
make_sequence<
3
>
==
sequence<
0
, 1
, 2
>
;
Maintenant pour l'utiliser, nous aurons à fournir deux constructeurs pour array_string : l’un va créer des séquences et l’autre les utilisera.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
template
<
int
N>
class
array_string
{
char
_array[N +
1
];
template
<
int
N1, int
... PACK1, int
... PACK2>
constexpr
array_string(const
string_literal<
N1>&
s1,
const
string_literal<
N -
N1>&
s2,
sequence<
PACK1...>
,
sequence<
PACK2...>
)
:
_array{
s1[PACK1]..., s2[PACK2]..., '
\0
'
}
{
}
public
:
template
<
int
N1, REQUIRES(N1 <=
N)>
constexpr
array_string(const
string_literal<
N1>&
s1,
const
string_literal<
N -
N1>&
s2)
// délègue à l'autre constructeur
:
array_string{
s1, s2, make_sequence<
N1>{}
,
make_sequence<
N -
N1>{}
}
{
}
}
;
Nous avons un constructeur privé qui fait l’initialisation actuelle et un constructeur public déléguant qui crée les séquences. Il peut le faire car les tailles des string_literal sont encodées dans leur type.
Notez les deux arguments de fonction additionnels dans le constructeur privé. Nous n’épellerons pas leur nom. Nous ne sommes pas intéressés par leurs valeurs à l’exécution, ni par leurs types : nous sommes seulement intéressés par être capable de déclarer deux packs de paramètres.
Maintenant si nous implémentons l’operator
[] et la fonction size :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
template
<
int
N>
class
array_string
{
char
_array[N +
1
];
public
:
constexpr
char
operator
[](int
i) const
{
return
X_ASSERT(i >=
0
&&
i <
N), _array[i];
}
constexpr
std::
size_t size() const
{
return
N;
}
// ...
}
;
Nous pouvons voir que notre outil fait la tâche basique :
2.
3.
4.
5.
6.
7.
constexpr
auto
S1 =
literal("ABCD"
);
constexpr
auto
S2 =
literal("EFGH"
);
constexpr
auto
R =
S1 +
S2;
static_assert
(R.size() ==
8
, "***"
);
static_assert
(R[0
] ==
'A'
, "***"
);
static_assert
(R[7
] ==
'H'
, "***"
);
Bien sûr, pour que l’outil soit complet, nous aurions à ajouter davantage de fonctions membres, comme la conversion vers des chaînes terminées par zéro, possiblement vers std::
string_view (pas du C++11 mais parfois disponible comme extension expérimentale) ou boost::string_view, et d’ajouter davantage de surcharges de l’opérateur concaténation fonctionnant pour différentes combinaisons de string_literal, array_string et const
char
*
. Mais il n’y a pas de place pour cela dans cet article. Au lieu de ceci, vous pouvez essayer une bibliothèque complètement implémentée.
V. Quelques observations▲
Quelqu'un pourra dire que quand j’ai le type array_string, je n’ai plus besoin du type string_literal, parce que le premier peut être utilisé partout à la place du second. C’est vrai, mais je peux trouver de la valeur en ne copiant pas le contenu de la chaîne, que le compilateur doit stocker de toute façon.
Au minimum, c’est juste une optimisation, array_string et string_literal, une fois créés, ont des interfaces identiques, ils diffèrent seulement sur la façon de stocker les caractères. Ils sont de types différents et exigent de moi de fournir davantage de surcharges pour l’opérateur concaténation avec une implémentation identique. D’une façon, c’est dans l’esprit du C++ : des compromis de performance dans l’implémentation sont des parts du contrat. Néanmoins, pour éviter la duplication, je peux faire de array_string et de string_literal deux instances du même patron : patrons de spécialisations :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
struct
RefImpl {}
; // classe tag
struct
ArrayImpl {}
; // classe tag
template
<
int
N, typename
Impl>
class
sstring // patron principal jamais utilisé
{
static_assert
(N !=
N, "***"
);
}
;
template
<
int
N>
class
sstring<
N, ArrayImpl>
{
char
_array[N +
1
];
// ...
}
;
template
<
int
N>
class
sstring<
N, RefImpl>
{
const
char
(&
_lit)[N +
1
];
// ...
}
;
template
<
int
N>
using
array_string =
sstring<
N, ArrayImpl>
;
template
<
int
N>
using
string_literal =
sstring<
N, RefImpl>
;
De cette façon, je peux fournir une implémentation d’opérateurs de concaténation pour toutes les combinaisons de array_string et de string_literal :
2.
3.
4.
5.
6.
7.
template
<
int
N1, int
N2, typename
Tag1, typename
Tag2>
constexpr
auto
operator
+
(const
sstring<
N1, Tag1>&
s1,
const
sstring<
N2, Tag2>&
s2)
->
array_string<
N1 +
N2>
{
return
array_string<
N1 +
N2>
(s1, s2);
}
Deuxièmement, j’ai mentionné que déduire la taille d’un string_literal depuis l’initialiseur est impossible en C++11. La situation sera différente en C++17. Je peux définir un guide de déduction :
2.
3.
4.
// Guide de déduction C++17 :
template
<
int
N_PLUS_1>
sstring(const
char
(&
lit)[N_PLUS_1]) // <- correspond à ceci
->
sstring<
N_PLUS_1 -
1
, RefImpl>
; // <- utilise ces arguments
Ceci dit, plus ou moins, « à chaque fois qu'une classe patron sstring est initialisée et qu’une partie ou tous ses paramètres patrons sont omis, et que l’argument utilisé pour l’initialisation correspond à ce que j’ai mis entre parenthèses, déduisez les paramètres de patrons restants comme indiqué. » Ceci sert le même but que la fonction literal, sauf que ceci sera utilisé implicitement dans des contextes comme ci-dessous :
constexpr
sstring S1 =
"ABCD"
;
constexpr
sstring S2 =
"EFGH"
;
constexpr
sstring R =
S1 +
S2;
On ne déduit pas seulement la taille, mais aussi quelle implémentation utiliser array_string ou string_literal. Notez que dans la dernière ligne, un autre guide de déduction est utilisé. Celui-ci est implicite et il n’a pas besoin d’être explicité par les programmeurs : quand j’initialise une instance de la classe patron sstring avec une autre instance de la même classe, déduisez exactement les mêmes arguments de patron.
Hormis la concaténation de forme, cette bibliothèque peut aussi offrir d’autres opérations, comme prendre une sous-chaîne, ce qui est facilement implémenté ; mais je n’ai jamais eu un besoin pour de telles choses donc, pour le moment, la bibliothèque implémente seulement ce que je sais dont on a besoin.
Aussi, notez que j’utilise le type int
pour représenter des tailles de chaînes, même si elles ne sont jamais négatives. C’est une habitude que je considère comme bonne. Quand on calcule une taille, par des opérations arithmétiques (au moment de la compilation), cela peut arriver que je soustraie un plus gros nombre d’un plus petit et que j'arrive à un résultat négatif. Si j’utilise des types non signés, de tels résultats sont convertis silencieusement à des valeurs positives (et incorrectes). Je ne veux pas cela : je veux que les tailles négatives soient explicitement capturées par des assertions statiques et reportées verbeusement par le compilateur.
VI. Remerciements▲
Toute l'équipe de Developpez.com remercie sincèrement Andrzej Krzemieński qui nous a aimablement permis de traduire et de publier son tutoriel sur notre site.
Nous remercions également stephane78l pour la traduction, chrtophe pour les retours techniques, f-leb pour ses corrections orthographiques et Winjerome pour le suivi de cette traduction.