5. Pièges courants lors de l'utilisation de tableaux.
5.1 Piège: Faire confiance aux liens de type non sûrs.
OK, on vous a dit, ou vous avez découvert vous-même, que les globaux (variables de portée d'espace de noms accessibles en dehors de l'unité de traduction) sont Evil ™. Mais saviez-vous à quel point ils sont vraiment mauvais ™? Considérez le programme ci-dessous, composé de deux fichiers [main.cpp] et [numbers.cpp]:
// [main.cpp]
#include <iostream>
extern int* numbers;
int main()
{
using namespace std;
for( int i = 0; i < 42; ++i )
{
cout << (i > 0? ", " : "") << numbers[i];
}
cout << endl;
}
// [numbers.cpp]
int numbers[42] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
Dans Windows 7, cela se compile et se lie très bien avec MinGW g ++ 4.4.1 et Visual C ++ 10.0.
Étant donné que les types ne correspondent pas, le programme se bloque lorsque vous l'exécutez.
Explication formelle: le programme a un comportement indéfini (UB), et au lieu de se bloquer, il peut donc simplement se bloquer, ou peut-être ne rien faire, ou il peut envoyer des e-mails menaçants aux présidents des États-Unis, de Russie, d'Inde, Chine et Suisse, et faites voler les démons nasaux hors de votre nez.
Explication pratique: dans main.cpp
le tableau est traité comme un pointeur, placé à la même adresse que le tableau. Pour un exécutable 32 bits, cela signifie que la première
int
valeur du tableau est traitée comme un pointeur. -À- dire, dans main.cpp
la
numbers
variable contient ou semble contenir, (int*)1
. Cela oblige le programme à accéder à la mémoire tout en bas de l'espace d'adressage, qui est classiquement réservé et à l'origine de pièges. Résultat: vous obtenez un crash.
Les compilateurs ont pleinement le droit de ne pas diagnostiquer cette erreur, car C ++ 11 §3.5 / 10 dit, à propos de l'exigence de types compatibles pour les déclarations,
[N3290 §3.5 / 10]
Une violation de cette règle sur l'identité de type ne nécessite pas de diagnostic.
Le même paragraphe détaille la variation autorisée:
… Les déclarations d'un objet tableau peuvent spécifier des types de tableau qui diffèrent par la présence ou l'absence d'une limite de tableau principale (8.3.4).
Cette variation autorisée n'inclut pas la déclaration d'un nom en tant que tableau dans une unité de traduction et en tant que pointeur dans une autre unité de traduction.
5.2 Piège: faire une optimisation prématurée ( memset
et amis).
Pas encore écrit
5.3 Piège: utiliser l'idiome C pour obtenir le nombre d'éléments.
Avec une profonde expérience en C, il est naturel d'écrire…
#define N_ITEMS( array ) (sizeof( array )/sizeof( array[0] ))
Puisqu'un array
désintègre pour pointer vers le premier élément si nécessaire, l'expression sizeof(a)/sizeof(a[0])
peut également être écrite comme
sizeof(a)/sizeof(*a)
. Cela signifie la même chose, et peu importe comment il est écrit, c'est l' idiome C pour trouver les éléments numériques du tableau.
Écueil principal: l'idiome C n'est pas de type sécurisé. Par exemple, le code…
#include <stdio.h>
#define N_ITEMS( array ) (sizeof( array )/sizeof( *array ))
void display( int const a[7] )
{
int const n = N_ITEMS( a ); // Oops.
printf( "%d elements.\n", n );
}
int main()
{
int const moohaha[] = {1, 2, 3, 4, 5, 6, 7};
printf( "%d elements, calling display...\n", N_ITEMS( moohaha ) );
display( moohaha );
}
passe un pointeur vers N_ITEMS
, et par conséquent produit très probablement un mauvais résultat. Compilé comme un exécutable 32 bits dans Windows 7, il produit…
7 éléments, appelant l'affichage ...
1 éléments.
- Le compilateur réécrit
int const a[7]
juste int const a[]
.
- Le compilateur réécrit
int const a[]
dans int const* a
.
N_ITEMS
est donc invoqué avec un pointeur.
- Pour un exécutable 32 bits
sizeof(array)
(taille d'un pointeur) vaut alors 4.
sizeof(*array)
est équivalent à sizeof(int)
, qui pour un exécutable 32 bits est également 4.
Afin de détecter cette erreur au moment de l'exécution, vous pouvez le faire…
#include <assert.h>
#include <typeinfo>
#define N_ITEMS( array ) ( \
assert(( \
"N_ITEMS requires an actual array as argument", \
typeid( array ) != typeid( &*array ) \
)), \
sizeof( array )/sizeof( *array ) \
)
7 éléments, appel de l'affichage ...
Échec de l'assertion: ("N_ITEMS nécessite un tableau réel comme argument", typeid (a)! = Typeid (& * a)), fichier runtime_detect ion.cpp, ligne 16
Cette application a demandé au Runtime de la terminer de manière inhabituelle.
Veuillez contacter l'équipe d'assistance de l'application pour plus d'informations.
La détection des erreurs d'exécution est meilleure que l'absence de détection, mais elle gaspille un peu de temps processeur et peut-être beaucoup plus de temps programmeur. Mieux avec la détection au moment de la compilation! Et si vous êtes heureux de ne pas prendre en charge les tableaux de types locaux avec C ++ 98, vous pouvez le faire:
#include <stddef.h>
typedef ptrdiff_t Size;
template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }
#define N_ITEMS( array ) n_items( array )
En compilant cette définition substituée dans le premier programme complet, avec g ++, j'ai eu…
M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: Dans la fonction 'void display (const int *)':
compile_time_detection.cpp: 14: erreur: pas de fonction correspondante pour l'appel à 'n_items (const int * &)'
M: \ count> _
Comment cela fonctionne: le tableau est transmis par référence à n_items
, et donc il ne se désintègre pas pour pointer vers le premier élément, et la fonction peut simplement renvoyer le nombre d'éléments spécifié par le type.
Avec C ++ 11, vous pouvez également l'utiliser pour les tableaux de type local, et c'est l' idiome C ++ de type sécurisé
pour trouver le nombre d'éléments d'un tableau.
5.4 Piège C ++ 11 et C ++ 14: Utilisation d'une constexpr
fonction de taille de tableau.
Avec C ++ 11 et versions ultérieures, c'est naturel, mais comme vous le verrez dangereux !, pour remplacer la fonction C ++ 03
typedef ptrdiff_t Size;
template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }
avec
using Size = ptrdiff_t;
template< class Type, Size n >
constexpr auto n_items( Type (&)[n] ) -> Size { return n; }
où le changement significatif est l'utilisation de constexpr
, qui permet à cette fonction de produire une constante de temps de compilation .
Par exemple, contrairement à la fonction C ++ 03, une telle constante de temps de compilation peut être utilisée pour déclarer un tableau de la même taille qu'un autre:
// Example 1
void foo()
{
int const x[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 4};
constexpr Size n = n_items( x );
int y[n] = {};
// Using y here.
}
Mais considérez ce code en utilisant la constexpr
version:
// Example 2
template< class Collection >
void foo( Collection const& c )
{
constexpr int n = n_items( c ); // Not in C++14!
// Use c here
}
auto main() -> int
{
int x[42];
foo( x );
}
Le piège: à partir de juillet 2015, ce qui précède se compile avec MinGW-64 5.1.0 avec
-pedantic-errors
, et, teste avec les compilateurs en ligne à gcc.godbolt.org/ , également avec clang 3.0 et clang 3.2, mais pas avec clang 3.3, 3.4. 1, 3.5.0, 3.5.1, 3.6 (rc1) ou 3.7 (expérimental). Et important pour la plate-forme Windows, il ne compile pas avec Visual C ++ 2015. La raison en est une instruction C ++ 11 / C ++ 14 sur l'utilisation des références dansconstexpr
expressions:
C ++ 11 C ++ 14 5,19 $ / 2 neuf
e tiret
Une expression conditionnelle e
est une expression constante de base à moins que l'évaluation de e
, suivant les règles de la machine abstraite (1.9), n'évalue l'une des expressions suivantes:
⋮
- une id-expression qui fait référence à une variable ou à un membre de données de type référence, sauf si la référence a une initialisation précédente et
- il est initialisé avec une expression constante ou
- il s'agit d'un membre de données non statique d'un objet dont la durée de vie a commencé dans le cadre de l'évaluation de e;
On peut toujours écrire le plus verbeux
// Example 3 -- limited
using Size = ptrdiff_t;
template< class Collection >
void foo( Collection const& c )
{
constexpr Size n = std::extent< decltype( c ) >::value;
// Use c here
}
… Mais cela échoue quand Collection
ne s'agit pas d'un tableau brut.
Pour traiter des collections qui peuvent être des non-tableaux, il faut la surcharge d'une
n_items
fonction, mais aussi, pour le temps de compilation, il faut une représentation du temps de compilation de la taille du tableau. Et la solution C ++ 03 classique, qui fonctionne bien également en C ++ 11 et C ++ 14, consiste à laisser la fonction rapporter son résultat non pas comme une valeur mais via son type de résultat de fonction . Par exemple, comme ceci:
// Example 4 - OK (not ideal, but portable and safe)
#include <array>
#include <stddef.h>
using Size = ptrdiff_t;
template< Size n >
struct Size_carrier
{
char sizer[n];
};
template< class Type, Size n >
auto static_n_items( Type (&)[n] )
-> Size_carrier<n>;
// No implementation, is used only at compile time.
template< class Type, size_t n > // size_t for g++
auto static_n_items( std::array<Type, n> const& )
-> Size_carrier<n>;
// No implementation, is used only at compile time.
#define STATIC_N_ITEMS( c ) \
static_cast<Size>( sizeof( static_n_items( c ).sizer ) )
template< class Collection >
void foo( Collection const& c )
{
constexpr Size n = STATIC_N_ITEMS( c );
// Use c here
(void) c;
}
auto main() -> int
{
int x[42];
std::array<int, 43> y;
foo( x );
foo( y );
}
A propos du choix du type de retour pour static_n_items
: ce code n'utilise pas std::integral_constant
car avec std::integral_constant
le résultat est représenté directement comme une constexpr
valeur, réintroduisant le problème d'origine. Au lieu d'une Size_carrier
classe, on peut laisser la fonction retourner directement une référence à un tableau. Cependant, tout le monde ne connaît pas cette syntaxe.
À propos de la dénomination: une partie de cette solution constexpr
-invalid-due-to-reference est de rendre explicite le choix de la constante de temps de compilation.
Espérons que le problème oops-there-was-a-reference-impliqué-in-your- constexpr
sera corrigé avec C ++ 17, mais jusque-là, une macro comme celle- STATIC_N_ITEMS
ci donne la portabilité, par exemple aux compilateurs clang et Visual C ++, en conservant le type sécurité.
Connexes: les macros ne respectent pas les étendues, donc pour éviter les collisions de noms, il peut être judicieux d'utiliser un préfixe de nom, par exemple MYLIB_STATIC_N_ITEMS
.