Articles tagués “initialisation

C vs C++ : initialisation des variables statiques

On lit souvent C/C++. Hé hé… S’il est indéniable que le ++ de C++ n’est pas là pour rien, il faut aussi savoir qu’il y a des différences en forme de pièges dans la partie « commune ». Ces différences sont particulièrement trompeuses quand on écrit du code C++ qu’on souhaite être compatible C. On va s’intéresser aujourd’hui à l’initialisation des variables statiques.

Voici un code C++ a priori anodin :

#include <stdlib.h>

int getValue()
{
    return rand();
}

int f(int c)
{
    static const int initial = getValue();

    if (initial != c)
        return c;
    else
        return initial;
}

int main()
{
    return f(42);
}

La ligne intéressante est la ligne 10 : on initialise une variable statique avec la valeur de retour d’une fonction.

Plaçons ce code dans un fichier main.cpp, plaçons ce fichier dans un projet Eclipse, et laçons un build :

[pre]16:52:21 **** Incremental Build of configuration Debug for project Experiences ****
Info: Internal Builder is used for build
g++ -std=c++0x -O0 -g3 -pedantic -Wall -Wextra
-c -o « src\\main.o » « ..\\src\\main.cpp »
g++ -o Experiences.exe « src\\main.o »

16:52:21 Build Finished (took 498ms)[/pre]

Renommons simplement le fichier pour forcer sa compilation en C. En effet, GCC se base sur l’extension du fichier pour savoir comment le compiler. Voici le résultat du build dans ce cas :

[pre]16:53:12 **** Incremental Build of configuration Debug for project Experiences ****
Info: Internal Builder is used for build
gcc -std=c99 -O0 -g3 -pedantic -Wall -Wextra
-c -o « src\\main.o » « ..\\src\\main.c »
..\src\main.c: In function ‘f’:
..\src\main.c:10:32: error: initializer element is not constant
static const int initial = getValue();
^

16:53:12 Build Finished (took 139ms)
[/pre]

L’erreur est claire. On pourrait soupçonner le qualificatif const mais cela ne change rien si on l’enlève. Le résultat est le même si la variable est statique au fichier au lieu d’être statique à la fonction.

Sans aller creuser dans les spécifications des langages, on se doute que C résoud la valeur d’une variable statique à la compilation alors que C++ autorise qu’elle soit résolue à l’exécution. Pour cela, le compilateur va sans doute générer un peu de code pour savoir si la variable a déjà été initialisée et l’initialiser le cas échéant (inutile de préciser que cela a un coût). On peut le vérifier en regardant le code assembleur de la fonction f() :

[pre] f(int):
0040144d: push %ebp
0040144e: mov %esp,%ebp
00401450: sub $0x18,%esp
10 static const int initial = getValue();
00401453: mov $0x40d020,%eax
00401458: movzbl (%eax),%eax
0040145b: test %al,%al
0040145d: jne 0x40148a
0040145f: movl $0x40d020,(%esp)
00401466: call 0x4014c4
0040146b: test %eax,%eax
0040146d: setne %al
00401470: test %al,%al
00401472: je 0x40148a
00401474: call 0x401440
00401479: mov %eax,0x40d028
0040147e: movl $0x40d020,(%esp)
00401485: call 0x4014bc
12 if (initial != c)
0040148a: mov 0x40d028,%eax
0040148f: cmp 0x8(%ebp),%eax
00401492: je 0x401499
13 return c;
00401494: mov 0x8(%ebp),%eax
00401497: jmp 0x40149e
15 return initial;
00401499: mov 0x40d028,%eax
16 }
[/pre]

Changeons l’initialisation de notre variable par quelque chose de constant, par exemple :

static const int initial = 666;

Voici le nouveau code assembleur, beaucoup plus simple, de la même fonction f() :
[pre] f(int):
0040144d: push %ebp
0040144e: mov %esp,%ebp
12 if (initial != c)
00401450: cmpl $0x29a,0x8(%ebp)
00401457: je 0x40145e
13 return c;
00401459: mov 0x8(%ebp),%eax
0040145c: jmp 0x401463
15 return initial;
0040145e: mov $0x29a,%eax
16 }
[/pre]

Pour avoir un code similaire en C, il faudrait implémenter une protection similaire, comme ceci :

#include <stdbool.h>
int f(int c)
{
    static int initial;
    static bool guard = false;

    if (guard == false)
    {
        guard = true;
        initial = rand();
    }

    if (initial != c)
        return c;
    else
        return initial;
}

On voit quelques inconvénients : perte de lisibilité, perte du qualificatif const, aucune garantie de thread-safety. A noter qu’en C++, le caractère thread-safe d’un tel code semble dépendre de la version du langage, comme débattu dans cette discussion stackoverflow « Is local static variable initialization thread-safe in C++11?« .

C’est tout pour aujourd’hui, vous êtes prévenus 🙂

Édition du 05/04/2017 : il s’avère en fait que la nécessité d’un initializer constant en C ne se limite pas aux variables déclarés avec le mot-clé static. Cette contrainte s’applique à toutes les variables ayant une static storage duration, comme expliqué dans cette discussion stackoverflow « Error “initializer element is not constant” when trying to initialize variable with const« . Ainsi, cette contrainte s’applique aussi aux variables globales.


Initialisation des tableaux et des structures en C

J’ai écrit un précédent article sur les designated initializers en C99. Je terminais cet article par une question : qu’advient-il des champs non désignés ? Quelle valeur ont-ils ? Il était temps de répondre à cette question et j’ai donc jeté un coup œil à la norme. Comme d’habitude, un coup d’œil se transforme en jeu de piste. Le cas particulier des designated initializers se résout en inspectant les règles d’initialisation des tableaux et des structures en général. Extraits choisis.

Commençons par faire simple

Il y a quelques règles assez connues en C pour l’initialisation des tableaux et des structures.

Commençons par l’état des variables non initialisées explicitement :

  • Les variables utilisant une allocation de type statique (c’est le cas des variables globales et des variables statiques) sont toujours initialisées à zéro de manière implicite.
  • Les variables avec une allocation automatique (celles déclarées sur la pile, dont la majorité des variables) sont laissées à une valeur indéfinie, qui dépend de l’état de la mémoire à leur création.

Ces deux règles s’appliquent d’ailleurs à tous les types de variables.

Les tableaux et les structures s’initialisent explicitement avec une liste de valeurs placée entre accolades et on sait que :

  • Les champs d’une structure doivent être donnés dans l’ordre (sauf si designated initializers).
  • Les valeurs remplissent les cases d’un tableau dans l’ordre à partir de l’indice 0 (même remarque).
  • Un tableau dont on ne précise pas la taille aura la même longueur que la liste d’initialisation, qui est obligatoire dans ce cas.
  • Si la liste d’initialisation ne contient pas autant de champs que la structure et ne contient pas de designated initializer, le compilateur émet généralement un warning: missing initializer.
  • Si la liste d’initialisation ne contient pas assez d’éléments pour initialiser toutes les cases d’un tableau, alors les cases restantes sont implicitement initialisées à zéro.
  • L’utilisation d’une liste entre accolades est possible uniquement à l’initialisation et est impossible lors d’une affectation ultérieure.

Voici un code regroupant ces différents exemples :

struct s
{
    int a;
    int b;
    int c;
    int d;
};

struct s sg;    // contenu vaut zero
int tg[5];      // idem

int main(void)
{
    struct s s1 = {12, .c=17};      // pas de warning
    struct s s2 = {12, 15};         // warning : missing initializer
    struct s s3 = {12, 17, 19, 21}; // pas de warning

    int t1[5] = {1,2,3};            // les deux derniers elements valent 0
    int t2[ ] = {1,2,3};            // t2 a une longueur de 3

    struct s s4;                    // contenu indefini
    int t3[5];                      // idem

//    int t4[];                     // Genere : "error: array size missing in 't4'"

    return s2.c + s2.d;             // --> return 0
}

Pour être rigoureux et tout expliquer

Explorons la norme pour vérifier tout ça.

Les tableaux et les structures sont désignés de manière commune par le terme de « type agrégés », comme indiqué au point 21 de la partie 6.2.5 Types :

Arithmetic types and pointer types are collectively called scalar types. Array
and structure types are collectively called aggregate types.37)

La note de bas de page 37 précise au passage qu’une union ne peut pas être considérée comme un type agrégé car :

37) Note that aggregate type does not include union type because an object
with union type can only contain one member at a time.

Allons maintenant à la partie 6.7.8 Initialization. On lit avec intérêt le point 10 :

If an object that has automatic storage duration is not initialized explicitly, its value is
indeterminate. If an object that has static storage duration is not initialized explicitly,
then:
— if it has pointer type, it is initialized to a null pointer;
— if it has arithmetic type, it is initialized to (positive or unsigned) zero;
— if it is an aggregate, every member is initialized (recursively) according to these rules;
— if it is a union, the first named member is initialized (recursively) according to these
rules.

On retrouve ici la règle que j’énonçais un peu plus haut : les variables globales et statiques sont initialisées implicitement à zéro (ou NULL) ; les autres contiennent tout et n’importe quoi.

A ce moment là, on se dit que c’est étonnant quand même d’avoir la fin de notre tableau initialisé à zéro et que notre programme renvoie 0 et non une valeur indéterminée. Ces éléments utilisent une classe d’allocation statique et ne sont pas initialisés explicitement : cette règle suppose que leur valeur est indéterminée. C’était sans compter sur le point 21 :

If there are fewer initializers in a brace-enclosed list than there are elements or members
of an aggregate, or fewer characters in a string literal used to initialize an array of known
size than there are elements in the array, the remainder of the aggregate shall be
initialized implicitly the same as objects that have static storage duration.

Si les éléments entre accolades ne sont pas assez nombreux pour remplir tout le type agrégé (champs d’une structure, cases d’un tableau) alors les champs restants sont initialisés comme si les variables étaient statiques et sont donc à NULL……..ou zéro ! CQFD.

C’est valable pour les structures (retour du programme ci-dessus) comme pour les tableaux, pour lesquels la norme donne le bel exemple suivant (point 26) :

EXAMPLE 3 The declaration

int y[4][3] = {
    { 1, 3, 5 },
    { 2, 4, 6 },
    { 3, 5, 7 },
};

is a definition with a fully bracketed initialization: 1, 3, and 5 initialize the first row of y (the array object y[0]), namely y[0][0], y[0][1], and y[0][2]. Likewise the next two lines initialize y[1] and y[2]. The initializer ends early, so y[3] is initialized with zeros.

Cette règle s’applique donc  aussi dans le cas des designated initializers, comme montré dans les points 36 et 37 :

36 EXAMPLE 12 Space can be ‘‘allocated’’ from both ends of an array by using a single designator:

int a[MAX] = {
    1, 3, 5, 7, 9, [MAX-5] = 8, 6, 4, 2, 0
};

37 In the above, if MAX is greater than ten, there will be some zero-valued elements in the middle; if it is less than ten, some of the values provided by the first five initializers will be overridden by the second five.

Pour finir, la confirmation que la longueur des tableaux, si elle n’est pas spécifiée, est donnée par la taille de la liste :

25 EXAMPLE 2 The declaration
int x[] = { 1, 3, 5 };
defines and initializes x as a one-dimensional array object that has three elements, as no size was specified
and there are three initializers.

La norme a encore frappé !

PS : Pour ceux d’entre vous qui n’auraient pas la norme sur votre bureau ou votre table de chevet et qui souhaiterait lire ce magnifique document, vous trouverez le PDF ici.

 


Designated Initializers en C

Une lecture sur Stackoverflow m’a fait découvrir des possibilités très intéressantes quant à l’initialisation des structures et des tableaux. Le C ANSI impose que l’ordre des champs dans l’initialisation d’une structure ou dans un tableau soit le même que l’ordre des éléments de cette structure ou de ce tableau. Par exemple, dans le code suivant :

int tableau[] = {0, 1, 3, 42};

les valeurs sont données dans l’ordre des indices (0 à l’indice 0, 1 à l’indice 1, 3 à l’indice 2 et 42 à l’indice 3). Impossible de définir uniquement la valeur 42 à l’indice 3.

Le C99 offre un mécanisme plus complexe pour spécifier les indices des tableaux ou les champs des structures auxquels appliquer les valeurs d’initialisation. Ce sont les designated initializers. GCC offre un mécanisme complémentaire pour autoriser l’initialisation d’un tableau grâce à une plage . Plutôt qu’un long discours, voici un joli code exemple !

#include <stdio.h>
#include <string.h>

typedef struct
{
    int entier;
    float flottant;
    short court;
} sData;

int main()
{
    unsigned int i;

    // Standard C99
    char dataC99[] = { [0] = 'a', 'c', [6] = 'b'};
    sData structC99 = {.flottant = 3.1415, .court = 42,  .entier = 65000};

    // Extension GNU
    char dataGNU[] = { [0 ... 3 ] = 'a', 'c', [6] = 'b'};

    puts("C99");
    for(i=0; i < sizeof(dataC99); putchar(dataC99[i++]) )
        ;
    printf("\nEntier = %d\tFlottant = %f\tCourt = %d", structC99.entier, structC99.flottant, structC99.court);

    puts("\n\nGNU");
    for(i=0; i < sizeof(dataGNU); putchar(dataGNU[i++]) )
        ;

    return 0;
}

J’utilise les options indispensables options -Wall -Wextra, ainsi que la contraignante -pedantic pour avoir le maximum d’information sur la compatibilité du code. Enfin, je spécifie que le code doit être compilé en C99 grâce à l’option -std=c99. Avec CodeBlocks sous Windows XP (utilisant le portage de GCC), le compilateur n’émet qu’un seul warning, qui correspond bien à l’extension GNU :

d:\…\developpez.c|21|warning: ISO C forbids specifying range of elements to initialize

Si on remplace -std=c99 par -ansi, on aura en plus toute une tripotée de warnings comme celui-ci :

d:\…\developpez.c|16|warning: ISO C90 forbids specifying subobject to initialize

Exemple d’application directe ? On peut initialiser un tableau  à une valeur particulière autre que zéro en toute simplicité :

int array[1024] = {[0 ... 1023] = 5};

Pour plus d’information, je vous renvoie à la documentation de GCC sur ces splendides designated initializers :

http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gcc/Designated-Inits.html

PS : Je n’ai pas encore réussi à déterminer si les champs non désignés étaient forcément égaux à 0, mais je vais tâcher de me renseigner 🙂