ProgrammationDéveloppeur Rust systèmes

Comment fonctionne l'évaluation constante (const evaluation) en Rust ? Quelles sont les différences entre const, static et let, dans quels cas peut-on (et doit-on) utiliser const fn, et quelles sont les limitations lors de l'écriture de calculs à l'étape de compilation ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

L'évaluation constante en Rust permet d'exécuter certaines parties des calculs ou de l'initialisation à l'étape de compilation, plutôt qu'au moment de l'exécution du programme.

  • const déclare une constante immuable, calculée à l'étape de compilation, et qui n'a pas d'adresse dans la mémoire :
const PI: f64 = 3.1415;
  • static déclare une variable globale, située dans un segment spécifique de la mémoire (généralement .data ou .bss), la mutabilité est possible (requiert unsafe) :
static mut GLOBAL_COUNTER: i32 = 0;
  • let est utilisé pour les variables sur la pile, leur valeur peut être calculée au moment de l'exécution, et elles doivent être initialisées avant la première utilisation.

const fn est une fonction dont le résultat peut être utilisé pour définir une valeur const ou static. Ces fonctions peuvent être appelées dans un contexte constant.

const fn factorial(n: usize) -> usize { if n == 0 { 1 } else { n * factorial(n - 1) } } const FACT_5: usize = factorial(5); // Se compile !

Limitations de const fn :

  • On ne peut utiliser que d'autres const fn,
  • On ne peut pas utiliser l'allocation sur le heap (Box::new et autres),
  • On ne peut pas appeler des opérations non sécurisées,
  • Il n'y a pas d'accès aux variables et fonctions externes (sauf si elles sont const fn).

Question piège.

Question : Peut-on utiliser n'importe quelle fonction dans un contexte const, si son résultat ne change pas ? Par exemple, comme ça :

fn add(a: i32, b: i32) -> i32 { a + b } const RES: i32 = add(1, 2);

Réponse fausse typique : Oui, car la fonction est pure et le résultat est connu à l'avance.

Réponse correcte : Non, la fonction doit être explicitement déclarée comme const fn, seulement alors elle peut être appelée dans une initialisation const. Les fonctions ordinaires ne sont appelées qu'à l'étape d'exécution !

Exemple :

const fn add(a: i32, b: i32) -> i32 { a + b } const RES: i32 = add(1, 2); // Se compile !

Exemples réels d'erreurs dues à l'ignorance des subtilités du sujet.


Histoire

Dans un projet avec des calculs de géométrie 3D, un développeur a essayé de déclarer un tableau de valeurs en utilisant le résultat d'un appel à une fonction ordinaire, plutôt qu'une const fn. En conséquence, des erreurs de compilation sont apparues, et le profit du calcul à l'étape de compilation a été perdu.


Histoire

L'utilisation de static mut pour un cache global a conduit à une concurrence de données lors des accès depuis plusieurs threads (static mut n'est pas sûr !). Il aurait fallu utiliser Atomic ou Mutex pour synchroniser l'accès à la ressource globale.


Histoire

Dans une tentative d'accélérer l'initialisation de grands tableaux, ceux-ci ont été définis comme static, mais on a oublié que static a toujours une adresse fixe, ce qui a empêché les données d'être mises en cache par le processeur comme local, et les opérations se sont ralenties sur le chemin chaud de la logique du serveur. Il aurait fallu utiliser des expressions let locales avec des calculs à l'exécution.