Concaténation de chaînes de caractères au moment de la compilation (C++11)

Andrzej Krzemieński

Traducteur

stephane78l

Dans ce tutoriel, vous allez apprendre à concaténer des chaînes de caractères au moment de la compilation de votre programme C++.

Commentez

- Titre : Concaténation de chaînes de caractères au moment de la compilation (C++11)

- Auteur : Andrzej Krzemieński

- Traducteur : stephane78l

- Parution : 26 avril 2019

- 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 ® 2019 Andrzej. 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. Le problème

- II. Que pouvons-nous faire ?

- III. Vers la solution idéale

- IV. Implémenter la concaténation de string au moment de la compilation

- V. Quelques observations

- VI. Remerciements

I - Le problème

Nous allons commencer par un bug, tiré de la vie réelle. Il englobe trois fichiers :

service.h

1. #include <string>

2. struct Service

3. {

4. static const std::string NAME;

5. };

service.cpp

1. #include "service.h"

2. const std::string Service::NAME = "SERVICE1";

main.cpp

1. #include "service.h"

2. #include <iostream>

3. const std::string MSG = "service " + Service::NAME + " ready";

4. int main()

5. {

6. std::cout << MSG << std::endl;

7. }

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

service.h

#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 :

service.h

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 :

main.cpp

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 :

run_before_main.cpp

const bool _ = run_message_loop(), true;
int main() {}

run_after_main.cpp

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 :

1. template <int N>

2. class string_literal

3. {

4. const char (&_lit) [N+1];

5. public:

6. // …

7. };

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 :

1. template <int N_PLUS_1>

2. constexpr auto literal(const char (&lit)[N_PLUS_1])

3. -> string_literal<N_PLUS_1 - 1>

4. {

5. return string_literal<N_PLUS_1 - 1>(lit);

6. }

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 :

1. // WRONG:

2. template <int N>

3. constexpr auto literal(const char (&lit)[N + 1])

4. -> string_literal<N>

5. {

6. return string_literal<N>(lit);

7. }

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 :

1. template <int N>

2. class string_literal

3. {

4. const char (&_lit)[N+1];

5. public:

6. constexpr char operator[](int i) const

7. {

8. return X_ASSERT(i >= 0 && i < N), _lit[i];

9. }

10. };

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 :

1. //en C++14

2. template <int N>

3. class array_string

4. {

5. char _array[N + 1];

7. public:

8. template <int N1, REQUIRES(N1 <= N)>

9. constexpr array_string(const string_literal<N1>&     s1,

10. const string_literal<N - N1>& s2)

11. {

12. for (int i = 0; i < N1; ++i)

13. _array[i] = s1[i];

15. for (int i = 0; i < N - N1; ++i)

16. _array[N1 + i] = s2[i];

18. _array[N] = '\0';

19. }

20. };

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 :

1. //en C++11

2. template <int N1, REQUIRES(N1 <= N)>

3. constexpr array_string(const string_literal<N1>&     s1,

4. const string_literal<N - N1>& s2)

5. : _array{ /* WHAT? */ }

6. {}

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 :

1. // Pas encore C++ (pas de PACk 1 ou PACK2)

2. template <int N1, REQUIRES(N1 <= N)>

3. constexpr array_string(const string_literal<N1>&     s1,

4. const string_literal<N - N1>& s2)

5. : _array{ s1[PACK1]..., s2[PACK2]..., '\0' }

6. {}

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 :

1. // le type utilisé pour recevoir le pack

3. template <int... I>

4. struct sequence {};

6. // meta-function auxiliaire pour faire des sequence de taille (N+1)

7. // depuis une sequence de taille N

9. template <typename T>

10. struct append;

12. template <int... I>

13. struct append<sequence<I...>>

14. {

15. using type = sequence<I..., sizeof...(I)>;

16. };

18. //  implementation recursive de  make_sequence

20. template <int I>

21. struct make_sequence_;

23. template <int I>

24. using make_sequence = typename make_sequence_<I>::type;

26. template <>

27. struct make_sequence_<0> // recursion end

28. {

29. using type = sequence<>;

30. };

32. template <int I>

33. struct make_sequence_ : append<make_sequence<I - 1>>

34. {

35. static_assert (I >= 0, "taille negative");

36. };

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.

1. template <int N>

2. class array_string

3. {

4. char _array[N + 1];

6. template <int N1, int... PACK1, int... PACK2>

7. constexpr array_string(const string_literal<N1>&     s1,

8. const string_literal<N - N1>& s2,

9. sequence<PACK1...>,

10. sequence<PACK2...>)

11. : _array{ s1[PACK1]..., s2[PACK2]..., '\0' }

12. {

13. }

15. public:

16. template <int N1, REQUIRES(N1 <= N)>

17. constexpr array_string(const string_literal<N1>&     s1,

18. const string_literal<N - N1>& s2)

19. // délègue à l'autre constructeur

20. : array_string{ s1, s2, make_sequence<N1>{},

21. make_sequence<N - N1>{} }

22. {

23. }

24. };

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 :

1. template <int N>

2. class array_string

3. {

4. char _array[N + 1];

6. public:

7. constexpr char operator[](int i) const

8. {

9. return X_ASSERT(i >= 0 && i < N), _array[i];

10. }

12. constexpr std::size_t size() const

13. {

14. return N;

15. }

16. // ...

17. };

Nous pouvons voir que notre outil fait la tâche basique :

1. constexpr auto S1 = literal("ABCD");

2. constexpr auto S2 = literal("EFGH");

3. constexpr auto R = S1 + S2;

5. static_assert(R.size() == 8, "***");

6. static_assert(R[0] == 'A', "***");

7. 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 :

1. struct RefImpl {};   // classe tag

2. struct ArrayImpl {}; // classe tag

4. template <int N, typename Impl>

5. class sstring // patron principal jamais utilisé

6. {

7. static_assert(N != N, "***");

8. };

10. template <int N>

11. class sstring<N, ArrayImpl>

12. {

13. char _array[N + 1];

14. // ...

15. };

17. template <int N>

18. class sstring<N, RefImpl>

19. {

20. const char (&_lit)[N + 1];

21. // ...

22. };

24. template <int N>

25. using array_string = sstring<N, ArrayImpl>;

27. template <int N>

28. 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 :

1. template <int N1, int N2, typename Tag1, typename Tag2>

2. constexpr auto operator+(const sstring<N1, Tag1>& s1,

3. const sstring<N2, Tag2>& s2)

4. -> array_string<N1 + N2>

5. {

6. return array_string<N1 + N2>(s1, s2);

7. }

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 :

1. // Guide de déduction C++17 :

2. template <int N_PLUS_1>

3. sstring(const char (&lit)[N_PLUS_1])   // <- correspond à ceci

4. -> 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.