Quelle est l'utilité du «vrai» dimensionnement des variables par C?

9

Une chose qui m'a toujours intuitivement frappé comme une caractéristique positive de C (enfin, en fait de ses implémentations comme gcc, clang, ...) est le fait qu'il ne stocke aucune information cachée à côté de vos propres variables lors de l'exécution. J'entends par là que si vous vouliez par exemple une variable "x" de type "uint16_t", vous pourriez être sûr que "x" n'occupera que 2 octets d'espace (et ne portera aucune information cachée comme son type, etc. .). De même, si vous vouliez un tableau de 100 entiers, vous pouvez être sûr qu'il est aussi grand que 100 entiers.

Cependant, plus je suis en train de trouver des cas concrets d'utilisation de cette fonction plus je me demande si elle a fait des avantages pratiques du tout. La seule chose que j'ai pu trouver jusqu'à présent est qu'il a évidemment besoin de moins de RAM. Pour les environnements limités, comme les puces AVR, etc., c'est certainement un énorme avantage, mais pour les cas d'utilisation de bureau / serveur de tous les jours, cela semble plutôt hors de propos. Une autre possibilité à laquelle je pense est qu'elle pourrait être utile / cruciale pour accéder au matériel, ou peut-être mapper des régions de mémoire (par exemple pour la sortie VGA et autres) ...?

Ma question: existe-t-il des domaines concrets qui ne peuvent pas ou ne peuvent être mis en œuvre que très lourdement sans cette fonctionnalité?

PS S'il vous plaît, dites-moi si vous avez un meilleur nom! ;)

Thomas Oltmann
la source
@gnat Je pense que je comprends quel est votre problème. C'est parce qu'il pourrait y avoir plusieurs réponses, non? Eh bien, je comprends que cette question pourrait ne pas convenir à la façon dont stackexchange fonctionne, mais honnêtement, je ne sais pas où demander autrement ...
Thomas Oltmann
1
@lxrec RTTI est stocké dans la table virtuelle et les objets ne stockent qu'un pointeur sur la table virtuelle. De plus, les types n'ont RTTI que s'ils ont déjà une table virtuelle car ils ont une virtualfonction membre. Ainsi, RTTI n'augmente jamais la taille des objets, il ne fait qu'agrandir le binaire d'une constante.
3
@ThomasOltmann Chaque objet qui a des méthodes virtuelles a besoin d'un pointeur vtable. Vous ne pouvez pas avoir les fonctionnalités des méthodes virtuelles sans cela. De plus, vous choisissez explicitement d'avoir des méthodes virtuelles (et donc une table virtuelle).
1
@ThomasOltmann Vous semblez très confus. Ce n'est pas un pointeur vers un objet qui porte un pointeur vtable, c'est l'objet lui-même. C'est-à-dire, a T *toujours la même taille et Tpeut contenir un champ caché qui pointe vers la table virtuelle. Et aucun compilateur C ++ n'a jamais inséré de vtables dans des objets qui n'en ont pas besoin.

Réponses:

5

Il y a plusieurs avantages, le plus évident étant au moment de la compilation pour s'assurer que des choses comme les paramètres de fonction correspondent aux valeurs transmises.

Mais je pense que vous demandez ce qui se passe à l'exécution.

Gardez à l'esprit que le compilateur créera un runtime qui intègre la connaissance des types de données dans les opérations qu'il effectue. Chaque bloc de données en mémoire peut ne pas être auto-descriptif, mais le code sait intrinsèquement ce que sont ces données (si vous avez fait votre travail correctement).

Au moment de l'exécution, les choses sont un peu différentes de ce que vous pensez.

Par exemple, ne supposez pas que seuls deux octets sont utilisés lorsque vous déclarez uint16_t. Selon le processeur et l'alignement des mots, il peut occuper 16, 32 ou 64 bits sur la pile. Vous constaterez peut-être que votre gamme de courts métrages consomme beaucoup plus de mémoire que prévu.

Cela peut être problématique dans certaines situations où vous devez référencer des données à des décalages spécifiques. Cela se produit lors de la communication entre deux systèmes ayant des architectures de processeur différentes, soit via une liaison sans fil, soit via des fichiers.

C vous permet de spécifier des structures avec une granularité au niveau du bit:

struct myMessage {
  uint8_t   first_bit: 1;
  uint8_t   second_bit: 1;
  uint8_t   padding:6;
  uint16_t  somethingUseful;
}

Cette structure est longue de trois octets, avec un court défini pour commencer à un décalage impair. Il devra également être emballé afin d'être exactement comme vous l'avez défini. Sinon, le compilateur alignera les membres par mot.

Le compilateur générera du code en arrière-plan pour extraire ces données et les copier dans un registre afin que vous puissiez faire des choses utiles avec.

Vous pouvez maintenant voir que chaque fois que mon programme accède à un membre de la structure myMessage, il saura exactement comment l'extraire et l'utiliser.

Cela peut devenir problématique et difficile à gérer lors de la communication entre différents systèmes avec différentes versions de logiciels. Vous devez soigneusement concevoir le système et le code pour vous assurer que les deux côtés ont exactement la même définition des types de données. Cela peut être assez difficile dans certains environnements. C'est là que vous avez besoin d'un meilleur protocole qui contient des données auto-descriptives telles que les tampons de protocole de Google .

Enfin, vous faites un bon point pour demander à quel point cela est important dans l'environnement de bureau / serveur. Cela dépend vraiment de la quantité de mémoire que vous prévoyez d'utiliser. Si vous effectuez quelque chose comme le traitement d'image, vous pouvez finir par utiliser une grande quantité de mémoire, ce qui peut affecter les performances de votre application. C'est certainement toujours une préoccupation dans l'environnement embarqué où la mémoire est limitée et il n'y a pas de mémoire virtuelle.

Tereus Scott
la source
2
"Vous constaterez peut-être que votre gamme de shorts consomme beaucoup plus de mémoire que prévu." C'est faux dans C: les tableaux sont garantis pour contenir leurs éléments d'une manière sans lacune. Oui, le tableau doit être correctement aligné, tout comme un seul short. Mais c'est une exigence unique pour le début du tableau, le reste est automatiquement aligné correctement en raison d'être consécutif.
cmaster - réintègre monica le
De plus, la syntaxe du remplissage est incorrecte, elle devrait l'être uint8_t padding: 6;, tout comme les deux premiers bits. Ou, plus clairement, juste le commentaire //6 bits of padding inserted by the compiler. La structure, comme vous l'avez écrite, a une taille d'au moins neuf octets, pas trois.
cmaster - réintègre monica le
9

Vous avez trouvé l'une des seules raisons pour lesquelles cela est utile: la cartographie des structures de données externes. Ceux -ci incluent des tampons vidéo mappés en mémoire, registres matériels, etc. Ils incluent également des données transmises à l' extérieur intact le programme, comme les certificats SSL, les paquets IP, des images JPEG, et à peu près toute autre structure de données qui a une vie persistante en dehors du programme.

Ross Patterson
la source
5

C est un langage de bas niveau, presque un assembleur portable, donc ses structures de données et ses constructions de langage sont proches du métal (les structures de données n'ont pas de coûts cachés - à l'exception des contraintes de remplissage, d'alignement et de taille imposées par le matériel et ABI ). Donc C n'a en effet pas de typage dynamique en natif. Mais si vous en avez besoin, vous pouvez adopter une convention selon laquelle toutes vos valeurs sont des agrégats commençant par des informations de type (par exemple certaines enum...); utilisez union-s et (pour les choses de type tableau) un membre de tableau flexible en structcontenant également la taille du tableau.

(lors de la programmation en C, il est de votre responsabilité de définir, documenter et suivre les conventions utiles - notamment les conditions préalables et postérieures et les invariants; également l'allocation dynamique de mémoire C nécessite des conventions explicites sur qui devrait freeune malloczone de mémoire empilée)

Ainsi, pour représenter des valeurs qui sont des nombres entiers ou des chaînes en boîte, ou une sorte de schéma -comme symbole , ou des vecteurs de valeurs, vous utiliserez conceptuellement une union étiquetée (mis en œuvre en tant qu'union de pointeurs) -Toujours à partir par le genre de type -, par exemple:

enum value_kind_en {V_NONE, V_INT, V_STRING, V_SYMBOL, V_VECTOR};
union value_en { // this union takes a word in memory
   const void* vptr; // generic pointer, e.g. to free it
   enum value_kind_en* vkind; // the value of *vkind decides which member to use
   struct intvalue_st* vint;
   struct strvalue_st* vstr;
   struct symbvalue_st* vsymb;
   struct vectvalue_st* vvect;
};
typedef union value_en value_t;
#define NULL_VALUE  ((value_t){NULL})
struct intvalue_st {
  enum value_kind_en kind; // always V_INT for intvalue_st
  int num;
};
struct strvalue_st {
  enum value_kind_en kind; // always V_STRING for strvalue_st
  const char*str;
};
struct symbvalue_st {
  enum value_kind_en kind; // V_SYMBOL
  struct strvalue_st* symbname;
  value_t symbvalue;
};
struct vectvalue_st {
  enum value_kind_en kind; // V_VECTOR;
  unsigned veclength;
  value_t veccomp[]; // flexible array of veclength components.
};

Pour obtenir le type dynamique d'une valeur

enum value_kind_en value_type(value_t v) {
  if (v.vptr != NULL) return *(v.vkind);
  else return V_NONE;
}

Voici une "distribution dynamique" aux vecteurs:

struct vectvalue_st* dyncast_vector (value_t v) {
   if (value_type(v) == V_VECTOR) return v->vvect;
   else return NULL;
}

et un "accesseur sûr" à l'intérieur des vecteurs:

value_t vector_nth(value_t v, unsigned rk) {
   struct vectvalue_st* vecp = dyncast_vector(v);
   if (vecp && rk < vecp->veclength) return vecp->veccomp[rk];
   else return NULL_VALUE;
}

Vous définissez généralement la plupart des fonctions courtes ci-dessus comme static inlinedans certains fichiers d'en-tête.

BTW, si vous pouvez utiliser le garbage collector de Boehm, vous pouvez alors coder assez facilement dans un style de niveau supérieur (mais dangereux), et plusieurs interpréteurs Scheme sont effectués de cette façon. Un constructeur de vecteur variadique pourrait être

value_t make_vector(unsigned size, ... /*value_t arguments*/) {
   struct vectvalue_st* vec = GC_MALLOC(sizeof(*vec)+size*sizeof(value));
   vec->kind = V_VECTOR;
   va_args args;
   va_start (args, size);
   for (unsigned ix=0; ix<size; ix++) 
     vec->veccomp[ix] = va_arg(args,value_t);
   va_end (args);
   return (value_t){vec};
}

et si vous avez trois variables

value_t v1 = somevalue(), v2 = otherval(), v3 = NULL_VALUE;

vous pouvez construire un vecteur à partir d'eux en utilisant make_vector(3,v1,v2,v3)

Si vous ne voulez pas utiliser le garbage collector de Boehm (ou concevoir le vôtre), vous devez faire très attention à définir les destructeurs et à documenter qui, comment et quand la mémoire doit être free-d; voir cet exemple. Vous pouvez donc utiliser malloc(mais ensuite tester contre son échec) au lieu de GC_MALLOCci - dessus, mais vous devez soigneusement définir et utiliser une fonction de destructeurvoid destroy_value(value_t)

La force de C est d'être suffisamment bas pour rendre possible le code comme ci-dessus et définir vos propres conventions (particulières à votre logiciel).

Basile Starynkevitch
la source
Je pense que vous avez mal compris ma question. Je ne veux pas de frappe dynamique en C. J'étais curieux de savoir si cette propriété spécifique de C est d'une utilité pratique.
Thomas Oltmann
Mais à quelle propriété exacte de C faites-vous référence? Les structures de données C sont proches du métal, donc n'ont pas de coûts cachés (sauf les contraintes d'alignement et de taille)
Basile Starynkevitch
Exactement cela: /
Thomas Oltmann
C a été inventé comme langage de bas niveau, mais lorsque les optimisations sont activées, les compilateurs comme gcc traitent un langage qui utilise la syntaxe de bas niveau mais ne fournit pas de manière fiable un accès de bas niveau aux garanties comportementales fournies par la plate-forme. On a besoin de sizeof pour utiliser malloc et memcpy, mais l'utilisation pour des calculs d'adresse plus sophistiqués peut ne pas être prise en charge dans le C. "moderne"
supercat