Annotations (Attributes)

Quelques raccourcis :

Depuis C++ 11, il est possible d'annoter certains passages de programmes C++ pour les complémenter avec un léger surcroît d'information. Ces annotations (en anglais, on utilise le terme Attributes).

Les annotations prennent la forme [[nom]] et permettent aux compilateurs de mieux faire leur travail, que ce soit en générant des avertissements plus clairs selon le contexte, en optimisant de manière plus efficace certains passages, ou simplement en clarifiant l'intention des programmeuses et des programmeurs.

L'idée derrière les annotations est que ce sont des énoncés complémentaires au programme : un compilateur doit pouvoir ne pas en tenir compte et demeurer capable de faire son travail. Ceci permet aux divers compilateurs d'offrir leurs annotations « maison » en plus de celles proposées à même le standard du langage.

La position officielle de WG21, votée à Albuquerque en 2017, est : « Attributes are only appropriate if they satisfy the guideline: "Compiling a valid program with all instances of a particular attribute ignored must result in a correct implementation of the original program" ».

Quelques exemples

Pour comprendre le rôle et l'utilité des annotations, voici quelques exemples simples, dont plusieurs (surtout ceux concernant des annotations de C++ 17) ont été empruntées à Jason Merrill.

Annotation [[noreturn]]

Depuis : C++ 11

Sur certains systèmes, particulièrement les systèmes embarqués où la mémoire n'est disponible qu'en quantité limitée, pouvoir générer un peu moins de code dans le cas de fonctions qui ne se compléteront jamais peut être fort utile. L'annotation [[noreturn]] informe un compilateur qu'il peut tenir compte de cette réalité et alléger le code généré lorsque le contexte s'y prête.

void critical_loop [[noreturn]] () {
   for(;;) {
      // ... code sans return
      // (throw est permis)
   }
}

Annotation [[carries_dependency]]

Depuis : C++ 11

Celle-ci est un peu obscure. Dans un programme utilisant des variables atomiques avec modèle memory_order_consume, où les transformations permises par les optimiseurs reposent sur sa compréhension des dépendances entre les données (Data Dependency), passer une adresse en paramètre à une fonction peut en faire perdre la trace par l'optimiseur, réduisant la qualité du code généré. L'annotation d'un tel paramètre par [[carries_dependency]] indique au compilateur qu'il vaut la peine de suivre la trace de ce paramètre de plus près.

void print(int *p) {
   cout << *p << endl;
}
void print2(int* [[carries_dependency]] p) {
   cout << *p << endl;
}
// ...
atomic<int*> p;
int* local=p.load(memory_order_consume);
// dépendance visible
if(local)
   cout << *local<< endl;
// dépendance opaque
if(local)
   print(local);
// dépendance visible
if(local)
   print2(local);

Annotation [[deprecated]]

Depuis : C++ 14

L'annotation [[deprecated]] permet d'indiquer qu'une fonction ou un type est sur le point d'être mis hors-service. Il est possible d'accompagner cette annotation d'un message, par [[deprecated("message")]], pour que le compilateur puisse relayer ce message lors de la génération des avertissements (ceci peut permettre de proposer un remplacement à ce qui est en cours de dépréciation).

[[deprecated]] int f() { return 3; }
[[deprecated("g() est obsolete, preferez trois()")]]
int g() { return 3; }
int trois() { return 3; }
int main() {
   f(); // message generique d'obsolescence
   g(); // message personnalise
   trois(); // pas de message
}

Annotation [[fallthrough]]

Depuis : C++ 17

L'annotation [[fallthrough]] informe le compilateur de l'absence volontaire d'un break dans une sélective (un switch). En effet, certains compilateurs (et plusieurs entreprises!) estiment que l'absence d'un break dans ces circonstances est source d'erreurs et de confusion, mais il se trouve qu'omettre le break peut être une saine pratique. Pour cette raison, [[fallthrough]] est une sorte de commentaire destiné au compilateur et l'informant du fait que cette omission est délibérée.

void f(char c) {
   switch(std::toupper(c, std::locale{""}) {
   case 'A': [[fallthrough]];
   case 'E': [[fallthrough]];
   case 'I': [[fallthrough]];
   case 'O': [[fallthrough]];
   case 'U': [[fallthrough]];
   case 'Y': traiter_voyelle(c);
   default:
      traiter_non_voyelle(c);
   }
}

Annotation [[likely]]

Depuis : C++ 20

L'annotation [[likely]] vise à indiquer au compilateur qu'un branchement devrait être optimisé comme étant probablement pris. Ceci peut viser plusieurs objectifs, incluant guider le compilateur dans la génération de code en présence d'informations connues de la programmeuse ou du programmeur mais absente du code source, ou encore demander l'optimisation d'une branche peu probable pour les cas où le cas rare doit être le cas le plus rapide (ce qui peut être le cas pour certains systèmes critiques).

void f(int n) {
  if(n < 0) {
     g();
  } [[likely]] else {
     h();
  }
}

Annotation [[nodiscard]]

Depuis : C++ 17

L'annotation [[nodiscard]] informe le compilateur du fait que le code client d'une fonction doit tenir compte de sa valeur de retour. Cette annotation peut décorer un type entier ou une fonction tout simplement.

class [[nodiscard]] Resultat { /* ... */ };
Resultat f();
int main() {
   f(); // provoquerait un avertissement
}

Depuis : C++ 20

L'annotation [[nodiscard]] peut être accompagnée d'une raison, pour guider la production de messages diagnostics pertinents.

class [[nodiscard("Discarding this result could lead to program termination")]] Resultat { /* ... */ };
Resultat f();
int main() {
   f(); // provoquerait un avertissement
}

Annotation [[maybe_unused]]

Depuis : C++ 17

L'annotation [[maybe_unused]] informe le compilateur qu'une fonction, une variable, un paramètre... peut ne pas être utilisé en pratique, et qu'il ne s'agit pas d'un accident.

void f(int a, [[maybe_unused]] int b) {
   // ici, ne pas utiliser a peut générér un avertissement
   // mais ne pas utiliser b pourrait ne pas en générer
}

Annotation [[optimize_for_synchronized]]

« Depuis » : la spécification technique de mémoire transactionnelle

L'annotation [[optimize_for_synchronized]] informe le compilateur qu'une fonction peut être optimisée agressivement lorsqu'elle est appelée depuis un bloc synchronized. Des exemples sont disponibles sur http://en.cppreference.com/w/cpp/language/transactional_memory

Directions futures

Certaines propositions d'annotations sont discutées pour des révisions futures du langage.

Annotation [[pure]]

Cette annotation indiquerait au compilateur qu'une fonction est pure, dans un sens se rapprochant de celui qu'on accorde aux fonctions en mathématiques (pas d'effet de bord, peut être parallélisée automatiquement, peut être mémoïsée, etc.)

Texte de Krister Walfridsson en 2016, portant sur l'extension __attribute__((pure)) de gcc et sur son interaction avec les exceptions : https://kristerw.blogspot.ca/2016/12/gcc-attributepure-and-c-exceptions.html

Annotation [[uninitialized]]

Cette annotation indiquerait au compilateur que le fait qu'une variable n'ait pas été initialisée est un geste délibéré.

Proposition formulée par Jonathan Müller en 2017 : http://foonathan.net/attribute-uninitialized-proposal.html

Annotation [[unlikely]]

Depuis : C++ 20

Voir [[likely]]. L'annotation [[unlikely]] marque un branchement comme devant être optimisé en tant que chemin peu probable.

Annotation [[unreachable]]

Cette annotation indiquerait au compilateur qu'une section de code est inatteignable en pratique, sans égard aux apparences.

Proposition formulée par Melissa Mears en 2017 : http://wg21.link/p0627

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !