Où sont stockées vos constantes sur le microcontrôleur CortexM (en utilisant le compilateur IAR C ++ comme exemple)

J'enseigne à mes élèves comment travailler avec le microcontrôleur STM32F411RE, à bord duquel il y a jusqu'à 512 Ko de ROM et 128 Ko de RAM
Habituellement, un programme est écrit en ROM sur ce microcontrôleur, et des données variables sont très souvent nécessaires en RAM pour que les constantes soient en ROM .
Dans le microcontrôleur ROM STM32F411RE, la mémoire est située aux adresses avec 0x08000000 ... 0x0807FFFF , et la RAM avec 0x20000000 ... 0x2001FFFF.

Et si tous les paramètres de l'éditeur de liens sont corrects, l'étudiant calcule que dans un code aussi simple, sa constante réside dans la ROM :

class WantToBeInROM { private: int i; public: WantToBeInROM(int value): i(value) {} int Get() const { return i; } }; const WantToBeInROM myConstInROM(10); int main() { std::cout << &myConstInROM << std::endl ; } 

Vous pouvez également essayer de répondre à la question: où est la constante myConstInROM en ROM ou en RAM ?

Si vous avez répondu à cette question qu'en ROM , je vous félicite, en fait très probablement vous vous trompez, la constante se situera généralement dans la RAM et pour comprendre comment placer correctement et correctement vos constantes dans la ROM - bienvenue au chat.

Présentation


Tout d'abord, une petite digression, pourquoi se soucier du tout.
Lors du développement de logiciels critiques pour la sécurité pour des appareils de mesure conformes à la CEI 61508-3: 2010 ou un équivalent domestique de la norme GOST CEI 61508-3-2018 , un certain nombre de points doivent être pris en compte qui ne sont pas critiques pour les logiciels conventionnels.

Le message principal de cette norme est que le logiciel doit détecter toute panne qui affecte la fiabilité du système et mettre le système en mode «crash»

En plus des défaillances mécaniques évidentes, par exemple, la défaillance ou la dégradation du capteur et la défaillance des composants électroniques, les erreurs causées par la défaillance de l'environnement logiciel, par exemple, le microcontrôleur RAM ou ROM , doivent être détectées.

Et si dans les deux premiers cas, il est possible de détecter une erreur uniquement de manière indirecte plutôt confuse (il existe des algorithmes qui déterminent la défaillance du capteur, par exemple, une méthode pour évaluer l'état d'un convertisseur thermique à résistance ), puis en cas de défaillance de l'environnement logiciel, cela peut être fait beaucoup plus facilement, par exemple, une défaillance de la mémoire peut être vérifier par un simple contrôle d'intégrité des données. Si l'intégrité des données est violée, interprétez cela comme une défaillance de la mémoire.

Si les données se trouvent pendant longtemps dans la RAM sans vérification ni mise à jour, la probabilité que quelque chose leur arrive en raison d'une défaillance de la RAM augmente avec le temps. Un exemple est certains coefficients d'étalonnage pour calculer la température qui ont été réglés en usine et écrits dans une EEPROM externe, au démarrage, ils sont lus et écrits dans la RAM et sont là jusqu'à ce que l'alimentation soit coupée. Et dans la vie, le capteur de température peut fonctionner pendant toute la période de l'intervalle d'étalonnage, jusqu'à 3-5 ans. De toute évidence, ces données RAM doivent être protégées et périodiquement vérifiées pour leur intégrité.

Mais il existe également des données, telles qu'une constante déclarée simplement pour la lisibilité, un objet d'un pilote LCD, SPI ou I2C, qui ne doit pas être modifié, est créé une fois et n'est pas supprimé jusqu'à la mise hors tension.

Ces données sont mieux conservées dans la ROM . Il est plus fiable du point de vue de la technologie et il est beaucoup plus facile de le vérifier, il suffit de lire périodiquement la somme de contrôle de toute la mémoire morte dans une tâche de faible priorité. Si la somme de contrôle ne correspond pas, vous pouvez simplement signaler l'échec de la ROM et le système de diagnostic affichera un accident.

Si ces données se trouvaient dans la RAM , il serait problématique, voire impossible, de déterminer leur intégrité en raison du fait qu'il n'est pas clair où les données immuables se trouvent dans la RAM et où elles sont modifiables, l'éditeur de liens les place comme il le souhaite, et pour protéger chaque objet RAM avec une somme de contrôle ressemble à la paranoïa.

Par conséquent, le moyen le plus simple est d'être sûr à 100% que les données constantes sont dans la ROM . Comment faire cela, je veux essayer d'expliquer. Mais vous devez d'abord parler de l'organisation de la mémoire dans ARM.

Organisation de la mémoire


Comme vous le savez, le cœur ARM a une architecture Harvard - les bus de données et de code sont séparés. Cela signifie généralement qu'il est supposé qu'il existe une mémoire distincte pour les programmes et une mémoire distincte pour les données. Mais le fait est que ARM est une architecture Harvard modifiée, c'est-à-dire l'accès à la mémoire s'effectue sur un bus, et le dispositif de gestion de la mémoire assure déjà la séparation des bus à l'aide de signaux de commande: lire, écrire ou sélectionner une zone mémoire.

Ainsi, les données et le code peuvent être dans la même zone de mémoire. Dans cet espace d'adressage unique peut être situé et la mémoire ROM et la RAM et les périphériques. Et cela signifie qu'en fait, le code et les données peuvent obtenir même là où cela dépend du compilateur et de l'éditeur de liens.

Par conséquent, afin de faire la distinction entre les zones de mémoire pour ROM (Flash) et RAM, elles sont généralement indiquées dans les paramètres de l'éditeur de liens, par exemple, dans IAR 8.40.1, cela ressemble à ceci:

 define symbol __ICFEDIT_region_ROM_start__ = 0x08000000; define symbol __ICFEDIT_region_ROM_end__ = 0x0807FFFF; define symbol __ICFEDIT_region_RAM_start__ = 0x20000000; define symbol __ICFEDIT_region_RAM_end__ = 0x2001FFFF; define region ROM_region = mem:[from __ICFEDIT_region_ROM_start__ to __ICFEDIT_region_ROM_end__]; define region RAM_region = mem:[from __ICFEDIT_region_RAM_start__ to __ICFEDIT_region_RAM_end__]; 

La RAM de ce microcontrôleur est située à 0x20000000 ... 0x2001FFF et la ROM à 0x008000000 ... 0x0807FFFF .
Vous pouvez facilement changer l'adresse de départ ROM_start en l'adresse RAM, disons RAM_start et l'adresse de fin ROM_end__ en RAM_end__ et votre programme sera complètement localisé dans la RAM.
Vous pouvez même faire le contraire et spécifier de la RAM dans la zone de mémoire ROM , et votre programme s'assemblera et clignotera avec succès, mais cela ne fonctionnera pas :)
Certains microcontrôleurs, tels que l'AVR, ont initialement un espace d'adressage séparé pour la mémoire de programme, la mémoire de données et les périphériques, et donc de telles astuces ne fonctionneront pas là-bas, et le programme est écrit sur ROM par défaut.

Tout l'espace d'adressage dans CortexM est unique et le code et les données peuvent être situés n'importe où. En utilisant les paramètres de l'éditeur de liens, vous pouvez définir la région pour les adresses ROM et RAM . IAR localise le segment de code .text dans la région ROM

Fichier objet et segments


Ci-dessus, j'ai mentionné le segment de code, voyons ce que c'est.

Un fichier objet distinct est créé pour chaque module compilé, qui contient les informations suivantes:

  • Segments de code et de données
  • Informations de débogage DWARF
  • Table de caractères

Nous nous intéressons au code et aux segments de données. Un segment est un tel élément contenant un morceau de code ou des données qui doivent être placés à une adresse physique en mémoire. Un segment peut contenir plusieurs fragments, généralement un fragment pour chaque variable ou fonction. Un segment peut être placé à la fois dans la ROM et la RAM .
Chaque segment a un nom et un attribut qui définit son contenu. L'attribut est utilisé pour définir un segment dans la configuration de l'éditeur de liens. Par exemple, les attributs peuvent être:

  • code - code exécutable
  • lecture seule - variables constantes
  • readwrite - variables initialisées
  • zeroinit - variables initialisées à zéro

Bien sûr, il existe d'autres types de segments, par exemple des segments contenant des informations de débogage, mais nous ne serons intéressés que par ceux qui contiennent du code ou des données de notre application.

En général, un segment est le plus petit bloc pouvant être lié. Cependant, si nécessaire, l'éditeur de liens peut également indiquer des blocs (fragments) encore plus petits. Nous ne considérerons pas cette option, nous le ferons avec des segments.

Pendant la compilation, les données et les fonctions sont placées dans différents segments. Et lors de la liaison, l'éditeur de liens attribue des adresses physiques réelles à différents segments. Le compilateur IAR a des noms de segments prédéfinis, dont certains que je fournirai ci-dessous:

  • .bss - Contient des variables statiques et globales initialisées à 0
  • .CSTACK - Contient la pile utilisée par le programme
  • .data - Contient des variables initialisées statiques et globales
  • .data_init - Contient les valeurs initiales des données dans la section .data si la directive d'initialisation pour l'éditeur de liens est utilisée
  • HEAP - Contient le tas utilisé pour héberger des données dynamiques
  • .intvec - Contient une table de vecteurs d'interruption
  • .rodata - Contient des données constantes
  • .text - Contient le code du programme

Afin de comprendre où se trouvent les constantes, nous ne nous intéresserons qu'aux segments
.rodata - un segment dans lequel les constantes sont stockées,
.data - un segment dans lequel toutes les variables statiques et globales initialisées sont stockées,
.bss - un segment dans lequel toutes les variables .data statiques et globales initialisées avec zéro (0) sont stockées,
.text - un segment pour stocker le code.

En pratique, cela signifie que si vous définissez la variable int val = 3 , la variable elle-même sera localisée par le compilateur dans le segment .data et marquée avec l'attribut readwrite , et le nombre 3 peut être placé soit dans le segment .text soit dans le segment .rodata ou, si une directive spéciale pour l'éditeur de liens dans .data_init est appliquée et est également marquée comme étant en lecture seule par elle .

Le segment .rodata contient des données constantes et comprend des variables constantes, des chaînes, des littéraux agrégés, etc. Et ce segment peut être placé n'importe où en mémoire.

Maintenant, il devient plus clair ce qui est prescrit dans les paramètres de l'éditeur de liens et pourquoi:

 place in ROM_region { readonly }; //   .rodata  .data_init (  )  ROM: place in RAM_region { readwrite, //   .data, .bss,  .noinit block STACK }; //  STACK  HEAP  RAM 

Autrement dit, toutes les données marquées avec l'attribut readonly doivent être placées dans ROM_region. Ainsi, les données de différents segments, mais marquées avec l'attribut readonly, peuvent entrer dans la ROM.

Eh bien, cela signifie que toutes les constantes doivent être en ROM, mais pourquoi dans notre code, au début de l'article, l'objet constant se trouve-t-il toujours dans la RAM?
 class WantToBeInROM { private: int i; public: WantToBeInROM(int value): i(value) {} int Get() const { return i; } }; const WantToBeInROM myConstInROM(10); int main() { std::cout << &myConstInROM << std::endl ; } 



Données constantes


Avant de clarifier la situation, rappelons d'abord que les variables globales sont créées en mémoire partagée, les variables locales, c'est-à-dire les variables déclarées à l'intérieur des fonctions "normales" sont créées sur la pile ou dans les registres, et les variables locales statiques sont également créées dans la mémoire partagée.

Qu'est-ce que cela signifie en C ++. Regardons un exemple:

 void foo(const int& C1, const int& C2, const int& C3, const int& C4, const int& C5, const int& C6) { std::cout << C1 << C2 << C3 << C4 << C5 << C6 << std::endl; } //     constexpr int Case1 = 1 ; //  (      ) const int Case2 = 2; int main() { //  . const int Case3 = 3 ; // . static const int Case4 = 4 ; //      ,     . constexpr int Case5 = Case1 + 5 ; //     . static constexpr int Case6 = 6 ; foo(Case1,Case2,Case3,Case4,Case5,Case6); return 1; } 

Ce sont toutes des données constantes. Mais pour chacun d'eux, la règle de création décrite ci-dessus s'applique, les variables locales sont créées sur la pile. Par conséquent, avec nos paramètres de l'éditeur de liens, cela devrait ressembler à ceci:

  • La constante globale Case1 doit être en ROM . Dans le segment .rodata
  • La constante globale Case2 doit être en ROM . Dans le segment .rodata
  • La constante locale Case3 doit se trouver dans la RAM (la constante a été créée sur la pile dans le segment STACK)
  • La constante statique de Case4 doit être dans la ROM . Dans le segment .rodata
  • La constante locale de Case5 doit se trouver dans la RAM (un cas intéressant, mais elle est exactement identique au cas 3.)
  • La constante statique de Case6 doit être dans la ROM . Dans le segment .rodata

Examinons maintenant les informations de débogage et le fichier de carte généré. Le débogueur indique à quelles adresses se trouvent ces constantes.

image

Comme je l'ai déjà dit, les adresses 0x0800 ... ce sont des adresses ROM , et 0x200 ... ce sont de la RAM . Voyons dans quels segments le compilateur a distribué ces constantes:

  .rodata const 0x800'4e2c 0x4 main.o //Case1 .rodata const 0x800'4e30 0x4 main.o //Case2 .rodata const 0x800'4e34 0x4 main.o //Case4 .rodata const 0x800'4e38 0x4 main.o //Case6 

Quatre constantes globales et statiques sont tombées dans le segment .rodata et deux variables locales ne sont pas tombées dans le fichier de carte car elles sont créées sur la pile et leur adresse correspond aux adresses de la pile. Le segment CSTACK commence à 0x2000'2488 et se termine à 0x2000'0488. Comme vous pouvez le voir sur l'image, les constantes sont juste créées au début de la pile.

Le compilateur place des constantes globales et statiques dans le segment .rodata , dont l'emplacement est spécifié dans les paramètres de l'éditeur de liens.

Il convient de noter un autre point important, l' initialisation . Les variables globales et statiques, y compris les constantes, doivent être initialisées. Et cela peut se faire de plusieurs manières. S'il s'agit d'une constante se trouvant dans le segment .rodata , l'initialisation se produit au stade de la compilation, c'est-à-dire la valeur est immédiatement écrite à l'adresse où se trouve la constante. S'il s'agit d'une variable régulière, l'initialisation peut se produire en copiant la valeur de la mémoire ROM à l'adresse de la variable globale:

Par exemple, si la variable globale int i = 3 définie, le compilateur l'a définie dans le segment de données .data , l'éditeur de liens l'a mise à 0x20000000:
.data inited 0x2000'0000 ,
et sa valeur d'initialisation (3) se trouvera dans le segment .rodata à l'adresse 0x8000190:
Initializer bytes const 0x800'0190
Si vous écrivez ce code:

 int i = 3; const int c = i; 

Il est évident que la constante globale n'est initialisée qu'après l'initialisation de la variable globale i , c'est-à-dire au moment de l'exécution. Dans ce cas, la constante sera située dans la RAM

Maintenant, si nous revenons à notre
exemple initial
 class WantToBeInROM { private: int i; public: WantToBeInROM(int value): i(value) {} int Get() const { return i; } }; const WantToBeInROM myConstInROM(10); int main() { std::cout << &myConstInROM << std::endl ; } 

Et nous nous demandons: dans quel segment le compilateur a-t-il défini l'objet constant myConstInROM ? Et nous obtenons la réponse: la constante se trouvera dans le segment .bss, contenant des variables statiques et globales initialisées à zéro (0).
.bss inited 0x2000'0004 0x4
myConstInROM 0x2000'0004 0x4


Pourquoi? Parce qu'en C ++, un objet de données qui est déclaré comme une constante et qui a besoin d'une initialisation dynamique est situé dans la mémoire en lecture-écriture et il sera initialisé au moment de la création.

Dans ce cas, l'initialisation dynamique se produit, const WantToBeInROM myConstInROM(10) , et le compilateur place cet objet dans le segment .bss , initialisant tous les champs 0 en premier, puis, lors de la création d'un objet constant, appelle le constructeur pour initialiser le champ i valeur 10.

Comment faire en sorte que le compilateur place notre objet dans le segment .rodata ? La réponse à cette question est simple, vous devez toujours effectuer une initialisation statique. Vous pouvez le faire de cette façon:

1. Dans notre exemple, on peut voir qu'en principe le compilateur peut optimiser l'initialisation dynamique en statique, puisque le constructeur est assez simple. Pour l'IAR du compilateur, vous pouvez marquer la constante avec l'attribut __ro_placement
__ro_placement const WantToBeInROM myConstInROM
Avec cette option, le compilateur placera la variable à l'adresse dans la ROM:
myConstInROM 0x800'0144 0x4 Data
De toute évidence, cette approche n'est pas universelle et généralement très spécifique. Par conséquent, nous passons à la bonne méthode :)

2. C'est pour faire un constructeur constexpr . Nous demandons immédiatement au compilateur d'utiliser l'initialisation statique, c'est-à-dire au stade de la compilation, lorsque l'ensemble de l'objet sera complètement "calculé" à l'avance et que tous ses champs seront connus. Tout ce que nous devons faire est d'ajouter constexpr au constructeur.

L'objet vole vers la ROM
 class WantToBeInROM { private: int i; public: constexpr WantToBeInROM(int value): i(value) {} int Get() const { return i; } }; const WantToBeInROM myConstInROM(10); int main() { std::cout << &myConstInROM << std::endl ; } 


Donc, pour être sûr que votre objet constant est dans la ROM, vous devez suivre des règles simples:
  1. Le segment .text dans lequel le code est placé doit être en ROM. Il est configuré dans les paramètres de l'éditeur de liens.
  2. Le segment .rodata dans lequel les constantes globales et statiques sont placées doit être en ROM. Il est configuré dans les paramètres de l'éditeur de liens.
  3. La constante doit être globale ou statique.
  4. Les attributs d'une classe de variables constantes ne doivent pas être modifiables
  5. L'initialisation de l'objet doit être statique, c'est-à-dire que le constructeur de la classe dont l'objet sera une constante doit être constexpr ou pas du tout défini (il n'y a pas d'initialisation dynamique)
  6. Si possible, si vous êtes sûr que l'objet doit être stocké dans la ROM au lieu de const, utilisez constexpr

Quelques mots sur constexpr et le constructeur constexpr. La principale différence entre const et constexpr est que l'initialisation de la variable const peut être retardée jusqu'à l'exécution. La variable constexpr doit être initialisée au moment de la compilation.
Toutes les variables constexpr sont de type const.

La définition du constructeur constexpr doit satisfaire aux exigences suivantes:
  • Une classe ne doit pas avoir de classes de base virtuelles.
     struct D2 : virtual BASE { //error, D2 must not have virtual base class. constexpr D2() : BASE(), mem(55) { } private: int mem; }; 
  • Chacun des types de paramètres de la classe doit être un type littéral.
  • Le corps du constructeur doit être = delete ou = default . Ou répondez aux exigences ci-dessous:
  • Il n'y a pas de blocs try catch dans le corps du constructeur.
  • Le corps constructeur peut utiliser nullptr
  • Le corps du constructeur peut utiliser static_assert
  • Dans le corps du constructeur, typedef peut être utilisé sans définir de classes ou d'énumérations
  • Le corps constructeur peut utiliser des directives et des déclarations en using
  • Chaque membre non statique d'une classe ou d'une classe de base doit être initialisé.
  • Les constructeurs d'une classe ou d'une classe de base, utilisés pour initialiser des éléments non statiques des membres de la classe et des sous-objets de la classe de base, doivent être constexpr .
  • Les initialiseurs de tous les éléments de données non statiques doivent être constexpr
  • Lors de l'initialisation des membres de classe, toutes les conversions de type doivent être valides dans une expression constante. Par exemple, l'utilisation de reinterpret_cast et la conversion de void* vers un autre type de pointeur n'est pas autorisée

Le constructeur implicite par défaut est le constructeur constexpr. Voyons maintenant quelques exemples:

Exemple 1. Objet dans la ROM
 class Test { private: int i; public: Test() {} ; int Get() const { return i + 1; } } ; const Test test; //  ROM.    . i  0  . int main() { std::cout << test.Get() << std::endl ; return 0; } 


Il vaut mieux ne pas écrire de cette façon, car dès que vous décidez d'initialiser l'attribut i, l'objet volera dans la RAM

Exemple 2. Un objet en RAM
 class Test { private: int i = 1; // i.       constexpr . public: Test() {} ; //       ,       ,  constexpr int Get() const { return i + 1; } } ; const Test test; //  RAM. i     int main() { std::cout << test.Get() << std::endl ; return 0; } 



Exemple 3. Un objet en RAM
 class Test { private: int i; public: Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; const Test test(10); //  RAM. i     int main() { std::cout << test.Get() << std::endl ; return 0; } 



Exemple 4. Objet dans la ROM
 class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; const Test test(10); //  ROM. i     constexpr  int main() { std::cout << test.Get() << std::endl ; return 0; } 



Exemple 5. Un objet en RAM
 class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; int main() { const Test test(10); //  RAM.    std::cout << test.Get() << std::endl ; return 0; } 



Exemple 6. Objet dans la ROM
 class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; int main() { static const Test test(10); //  ROM.    std::cout << test.Get() << std::endl ; return 0; } 



Exemple 7. Erreur de compilation
 class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get() // Get  ,  ,       (i),     .   ,       . { return i + 1; } } ; const Test test(10); int main() { std::cout << test.Get() << std::endl ; return 0; } 



Exemple 8. Un objet en ROM, héritant d'une classe abstraite
 class ITest { private: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class Test: public ITest { private: int i; public: constexpr Test(int value): i(value), ITest(value+1) {} ; int Get() const override { return i + 1; } } ; const Test test(10); //  ROM. i     constexpr , j   constexpr  ITest int main() { std::cout << test.Give() << std::endl ; return 0; } 



Exemple 9. Un objet dans la ROM agrège un objet situé dans la RAM
 class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) { k = value; j = value + 10; } } ; TestImpl testImpl(1); //    RAM. class Test: public ITest { private: int i; TestImpl & obj; //    public: constexpr Test(int value, TestImpl & ref): i(value), obj(ref), ITest(value+1) { } ; int Get() const override { return i + 1; } bool Set() const { obj.Set(100) ; //     return true; } } ; constexpr Test test(10, testImpl); //  ROM.     constexpr  int main() { std::cout << test.Set() << std::endl ; return 0; } 



Exemple 10. Le même objet mais statique dans la ROM
 class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) { k = value; j = value + 10; } } ; class Test: public ITest { private: int i; TestImpl & obj; //    public: constexpr Test(int value, TestImpl & ref): i(value), obj(ref),ITest(value+1) { } ; int Get() const override { return i + 1; } bool Set() const { obj.Set(100) ; //     return true; } } ; int main() { static TestImpl testImpl(1); //  static constexpr Test test(10, testImpl); //    ROM.     constexpr  std::cout << test.Set() << std::endl ; return 0; } 



Exemple 11. Et maintenant l'objet constant est non statique et donc en RAM
 class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) { k = value; j = value + 10; } } ; class Test: public ITest { private: int i; TestImpl & obj; //    public: constexpr Test(int value, TestImpl & ref): i(value), obj(ref),ITest(value+1) { } ; int Get() const override { return i + 1; } bool Set() const { obj.Set(100) ; //     return true; } } ; int main() { static TestImpl testImpl(1); //  const Test test(10, testImpl); //    RAM. std::cout << test.Set() << std::endl ; return 0; } 



Exemple 12. Erreur de compilation.
 class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) { k = value; j = value + 10; } } ; class Test: public ITest { private: int i; TestImpl obj; //   TestImpl public: constexpr Test(int value): i(value), obj(TestImpl(value)), //   constexpr   TestImpl ITest(value+1) { } ; int Get() const { return i + 1; } bool Set() const { obj.Set(100) ; //     return true; } } ; int main() { static TestImpl testImpl(1); //  static const Test test(10); //   std::cout << test.Set() << std::endl ; return 0; } 



Exemple 13. Erreur de compilation
 class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: constexpr TestImpl(int value): k(value), ITest(value) //   constexpr { } int Get() const override { return j + 10; } void Set(int value) //   ,  k  j.       ROM,        RAM { k = value; j = value + 10; } } ; class Test: public ITest { private: int i; TestImpl obj; public: constexpr Test(int value): i(value), obj(TestImpl(value)), // constexpr     obj,    obj  .rodata . ITest(value+1) { } ; int Get() const { return i + 1; } bool Set() const { obj.Set(100) ; //        constexpr     return true; } } ; int main() { static TestImpl testImpl(1); //  static const Test test(10); //   std::cout << test.Set() << std::endl ; return 0; } 



Exemple 14. Un objet dans la ROM
 class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: constexpr TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) const //   const { //do something } } ; class Test: public ITest { private: int i; const TestImpl obj; // ,    constexpr  public: constexpr Test(int value): i(value), obj(TestImpl(value)), ITest(value+1) { } ; int Get() const { return i + 1; } bool Set() const { obj.Set(100) ; //   return true; } } ; int main() { //static TestImpl testImpl(1); //  static const Test test(10); //    ROM.     constexpr  std::cout << test.Set() << std::endl ; return 0; } 



Et enfin, un objet constant contenant un tableau, avec l'initialisation du tableau via une fonction constexpr.
 class Test { private: int k[100]; constexpr void InitArray() { int i = 0; for(auto& it: k) { it = i++ ; } } public: constexpr Test(): k() { InitArray(); // constexpr     } int Get(int index) const { return k[index]; } } ; int main() { static const Test test; //    ROM.     ,  constexpr . std::cout << test.Get(10) << std::endl ; return 0; } 


Références:
Guide de développement IAR C / C ++
Constructeurs Constexpr (C ++ 11)
constexpr (C ++)

PS.
Après une discussion très utile avec Valdaros, vous devez ajouter les constantes tangentes ponctuelles suivantes. Selon la norme C ++ et ce document N1076.pdf

1. Toute modification d'un objet constant (à l'exception des membres mutables d'une classe) au cours de sa vie entraîne un comportement indéfini. C'est-à-dire

  const int ci = 1 ; int* iptr = const_cast<int*>(&ci); //UB,       *iptr = 2 ; 

  int i = 1; const int* ci = &i ; int* iptr = const_cast<int *> (ci); //   *iptr = 2 ; // UB,   i 

2. Le problème est que cela ne fonctionne que pendant toute la vie d'un objet constant, mais dans le constructeur et le destructeur, cela ne fonctionne pas. Par conséquent, il est tout à fait légitime de le faire:

 class Test { public: int i; constexpr Test(): i(0) { foo(this) ; } } ; Test *test1; constexpr void foo(Test* value) { value->i = 1; //       0  1 test1 = value ; //           } const Test test; int main() { test1->i = 2; //          2. std::cout << &test << std::endl; } 

Et c'est considéré comme légal. Malgré le fait que nous avons utilisé le constructeur constexpr et la fonction constexpr. L'objet va directement à la RAM.

Pour éviter cela, utilisez const-constexpr au lieu de const, puis il y aura une erreur de compilation qui vous dira que quelque chose ne va pas et que l'objet ne peut pas être constant.

 class Test { public: int i; constexpr Test(): i(0) { foo(this) ; } } ; Test *test1; constexpr void foo(Test* value) { value->i = 1; //       0  1 test1 = value ; //           } constexpr Test test; // / Error[Pe2400]: calling the default constructor for "Test" does not produce a constant value main.cpp 151 int main() { test1->i = 2; //          2. std::cout << &test << std::endl; } 

Source: https://habr.com/ru/post/fr453262/


All Articles