Wo sind Ihre Konstanten auf dem CortexM-Mikrocontroller gespeichert (am Beispiel des C ++ IAR-Compilers)?

Ich bringe meinen Schülern den Umgang mit dem Mikrocontroller STM32F411RE bei, an dessen Bord sich bis zu 512 kB ROM und 128 kB RAM befinden
Normalerweise wird auf diesem Mikrocontroller ein Programm in den ROM- Speicher geschrieben, und im RAM sind die zu ändernden Daten sehr oft erforderlich, um die Konstanten im ROM zu erstellen.
Im STM32F411RE, ROM- Mikrocontroller, befindet sich der Speicher an Adressen mit 0x08000000 ... 0x0807FFFF und RAM mit 0x20000000 ... 0x2001FFFF.

Und wenn alle Linker-Einstellungen korrekt sind, berechnet der Schüler, dass in einem so einfachen Code seine Konstante im ROM liegt:

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 ; } 

Sie können auch versuchen, die Frage zu beantworten: Wo befindet sich die Konstante myConstInROM im ROM oder im RAM ?

Wenn Sie diese Frage im ROM beantwortet haben, gratuliere ich Ihnen, wahrscheinlich liegt die Konstante im Allgemeinen im RAM, und um herauszufinden, wie Sie Ihre Konstanten korrekt und korrekt im ROM platzieren können - willkommen bei cat.

Einführung


Zunächst ein kleiner Exkurs, warum sollte man sich überhaupt darum kümmern?
Bei der Entwicklung sicherheitskritischer Software für Messgeräte, die IEC 61508-3: 2010 oder einem inländischen Äquivalent von GOST IEC 61508-3-2018 entsprechen , muss eine Reihe von Punkten berücksichtigt werden, die für herkömmliche Software nicht kritisch sind.

Die Hauptbotschaft dieses Standards lautet, dass die Software jeden Fehler erkennen muss, der die Zuverlässigkeit des Systems beeinträchtigt, und das System in den "Absturz" -Modus versetzen muss

Zusätzlich zu offensichtlichen mechanischen Fehlern, z. B. Sensorausfall oder -verschlechterung und Ausfall elektronischer Komponenten, sollten Fehler erkannt werden, die durch den Ausfall der Softwareumgebung verursacht werden, z. B. RAM- oder ROM- Mikrocontroller.

Und wenn es in den ersten beiden Fällen möglich ist, einen Fehler nur auf ziemlich verwirrte indirekte Weise zu erkennen (es gibt Algorithmen, die den Sensorausfall bestimmen, beispielsweise eine Methode zur Beurteilung des Zustands eines Widerstandsthermokonverters ), kann dies im Falle eines Softwareumgebungsausfalls viel einfacher durchgeführt werden, beispielsweise kann ein Speicherfehler sein Überprüfen Sie dies durch eine einfache Überprüfung der Datenintegrität. Wenn die Integrität der Daten verletzt wird, interpretieren Sie dies als Speicherfehler.

Wenn die Daten für eine lange Zeit im RAM liegen, ohne sie zu überprüfen und zu aktualisieren, steigt die Wahrscheinlichkeit, dass ihnen aufgrund eines RAM- Fehlers etwas passiert, mit der Zeit. Ein Beispiel sind einige Kalibrierungskoeffizienten zur Berechnung der Temperatur, die werkseitig eingestellt und in ein externes EEPROM geschrieben wurden. Beim Start werden sie gelesen und in den RAM geschrieben und sind dort, bis die Stromversorgung ausgeschaltet wird. Im Leben kann der Temperatursensor über den gesamten Zeitraum des Interkalibrierungsintervalls bis zu 3-5 Jahre arbeiten. Offensichtlich müssen solche RAM- Daten geschützt und regelmäßig auf ihre Integrität überprüft werden.

Es gibt aber auch Daten, wie eine Konstante, die nur zur besseren Lesbarkeit deklariert wurde, ein Objekt eines LCD-Treibers, SPI oder I2C, das nicht geändert werden sollte, einmal erstellt und erst nach dem Ausschalten gelöscht werden.

Diese Daten werden am besten im ROM gespeichert. Es ist aus technologischer Sicht zuverlässiger und viel einfacher zu überprüfen. Es reicht aus, die Prüfsumme des gesamten Nur-Lese-Speichers in einer Aufgabe mit niedriger Priorität regelmäßig zu lesen. Wenn die Prüfsumme nicht übereinstimmt, können Sie einfach den ROM- Fehler melden und das Diagnosesystem zeigt einen Unfall an.

Wenn diese Daten im RAM liegen , wäre es problematisch oder sogar unmöglich, ihre Integrität zu bestimmen, da nicht klar ist, wo sich die unveränderlichen Daten im RAM befinden und wo sie veränderbar sind. Der Linker platziert sie wie gewünscht und schützt jedes RAM-Objekt mit einer Prüfsumme Paranoia.

Daher ist es am einfachsten, 100% sicher zu sein, dass sich die konstanten Daten im ROM befinden . Wie das geht, möchte ich versuchen zu erklären. Aber zuerst müssen Sie über die Organisation des Speichers in ARM sprechen.

Speicherorganisation


Wie Sie wissen, verfügt der ARM-Kern über eine Harvard-Architektur - die Daten- und Codebusse sind getrennt. Normalerweise bedeutet dies, dass angenommen wird, dass es einen separaten Speicher für Programme und einen separaten Speicher für Daten gibt. Tatsache ist jedoch, dass ARM eine modifizierte Harvard-Architektur ist, d.h. Der Zugriff auf den Speicher erfolgt über einen Bus, und die Speicherverwaltungsvorrichtung bietet bereits die Trennung von Bussen mithilfe von Steuersignalen: Lesen, Schreiben oder Auswählen eines Speicherbereichs.

Somit können sich Daten und Code im selben Speicherbereich befinden. In diesem einzelnen Adressraum können sich ROM- Speicher sowie RAM und Peripheriegeräte befinden. Und das bedeutet, dass sowohl der Code als auch die Daten auch dort ankommen können, wo es vom Compiler und Linker abhängt.

Um zwischen den Speicherbereichen für ROM (Flash) und RAM zu unterscheiden, werden diese normalerweise in den Linker-Einstellungen angezeigt, z. B. in IAR 8.40.1 sieht dies folgendermaßen aus:

 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__]; 

Der RAM in diesem Mikrocontroller befindet sich bei 0x20000000 ... 0x2001FFF und der ROM bei 0x008000000 ... 0x0807FFFF .
Sie können die Startadresse ROM_start einfach in die RAM-Adresse ändern, z. B. RAM_start und die Endadresse ROM_end__ in RAM_end__, und Ihr Programm befindet sich vollständig im RAM.
Sie können sogar das Gegenteil tun und RAM im ROM- Speicherbereich angeben, und Ihr Programm wird erfolgreich zusammengestellt und geflasht, obwohl es nicht funktioniert :)
Einige Mikrocontroller, wie z. B. AVR, verfügen zunächst über einen separaten Adressraum für Programmspeicher, Datenspeicher und Peripheriegeräte. Daher funktionieren solche Tricks dort nicht, und das Programm wird standardmäßig in das ROM geschrieben.

Der gesamte Adressraum in CortexM ist einzeln, und Code und Daten können sich überall befinden. Mit den Linker-Einstellungen können Sie die Region für die ROM- und RAM- Adressen festlegen. IAR lokalisiert das Textcodesegment im ROM- Bereich

Objektdatei und Segmente


Oben habe ich das Codesegment erwähnt. Mal sehen, was es ist.

Für jedes kompilierte Modul wird eine separate Objektdatei erstellt, die die folgenden Informationen enthält:

  • Code- und Datensegmente
  • DWARF-Debugging-Informationen
  • Zeichentabelle

Wir interessieren uns für Code- und Datensegmente . Ein Segment ist ein solches Element, das einen Code oder Daten enthält, die an einer physischen Adresse im Speicher abgelegt werden müssen. Ein Segment kann mehrere Fragmente enthalten, normalerweise ein Fragment für jede Variable oder Funktion. Ein Segment kann sowohl im ROM als auch im RAM platziert werden .
Jedes Segment hat einen Namen und ein Attribut, das seinen Inhalt definiert. Das Attribut wird verwendet, um ein Segment in der Konfiguration für den Linker zu definieren. Zum Beispiel können Attribute sein:

  • Code - ausführbarer Code
  • schreibgeschützt - konstante Variablen
  • readwrite - initialisierte Variablen
  • zeroinit - nullinitialisierte Variablen

Natürlich gibt es andere Arten von Segmenten, zum Beispiel Segmente, die Debugging-Informationen enthalten, aber wir werden nur an solchen interessiert sein, die Code oder Daten aus unserer Anwendung enthalten.

Im Allgemeinen ist ein Segment der kleinste verknüpfbare Block. Bei Bedarf kann der Linker jedoch auch noch kleinere Blöcke (Fragmente) anzeigen. Wir werden diese Option nicht in Betracht ziehen, wir werden uns mit Segmenten befassen.

Während der Kompilierung werden Daten und Funktionen in verschiedenen Segmenten platziert. Während der Verknüpfung weist der Linker verschiedenen Segmenten echte physische Adressen zu. Der IAR-Compiler verfügt über vordefinierte Segmentnamen, von denen einige im Folgenden aufgeführt werden:

  • .bss - Enthält statische und globale Variablen, die auf 0 initialisiert wurden
  • .CSTACK - Enthält den vom Programm verwendeten Stapel
  • .data - Enthält statische und global initialisierte Variablen
  • .data_init - Enthält die Anfangswerte für die Daten im Abschnitt .data, wenn die Initialisierungsanweisung für den Linker verwendet wird
  • HEAP - Enthält den Heap, der zum Hosten dynamischer Daten verwendet wird
  • .intvec - Enthält eine Interrupt- Vektortabelle
  • .rodata - Enthält konstante Daten
  • .text - Enthält Programmcode

Um zu verstehen, wo sich die Konstanten befinden, werden wir uns nur für Segmente interessieren
.rodata - ein Segment, in dem Konstanten gespeichert sind,
.data - ein Segment, in dem alle initialisierten statischen und globalen Variablen gespeichert sind.
.bss - ein Segment, in dem alle statischen und globalen .data- Variablen gespeichert sind, die mit Null (0) initialisiert wurden.
.text - ein Segment zum Speichern von Code.

In der Praxis bedeutet dies, dass, wenn Sie die Variable int val = 3 , die Variable selbst vom Compiler im .data- Segment lokalisiert und mit dem readwrite- Attribut markiert wird. Die Nummer 3 kann entweder im .text- Segment oder im .rodata- Segment oder, wenn, platziert werden Eine spezielle Direktive für den Linker in .data_init wird angewendet und auch von dieser als schreibgeschützt markiert.

Das .rodata- Segment enthält konstante Daten und enthält konstante Variablen, Zeichenfolgen, aggregierte Literale usw. Und dieses Segment kann überall im Speicher abgelegt werden.

Jetzt wird klarer, was in den Linker-Einstellungen vorgeschrieben ist und warum:

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

Das heißt, alle mit dem Attribut readonly gekennzeichneten Daten sollten in ROM_region abgelegt werden. Somit können Daten aus verschiedenen Segmenten, die jedoch mit dem Attribut readonly gekennzeichnet sind, in das ROM gelangen.

Nun, das bedeutet, dass alle Konstanten im ROM sein müssen, aber warum liegt in unserem Code am Anfang des Artikels das konstante Objekt immer noch im 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 ; } 



Konstante Daten


Bevor wir die Situation klären, erinnern wir uns zunächst daran, dass globale Variablen im gemeinsamen Speicher erstellt werden, lokale Variablen, d. H. Variablen, die in "normalen" Funktionen deklariert sind, werden auf dem Stapel oder in Registern erstellt, und statische lokale Variablen werden auch im gemeinsam genutzten Speicher erstellt.

Was bedeutet das in C ++? Schauen wir uns ein Beispiel an:

 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; } 

Dies sind alles konstante Daten. Für jeden von ihnen gilt jedoch die oben beschriebene Erstellungsregel. Lokale Variablen werden auf dem Stapel erstellt. Daher sollte es mit unseren Linker-Einstellungen folgendermaßen aussehen:

  • Die globale Konstante von Fall1 muss sich im ROM befinden . Im Segment .rodata
  • Die globale Konstante von Case2 muss sich im ROM befinden . Im Segment .rodata
  • Die lokale Konstante von Fall 3 muss im RAM liegen (die Konstante wurde auf dem Stapel im STACK-Segment erstellt).
  • Die statische Konstante von Case4 muss sich im ROM befinden . Im Segment .rodata
  • Die lokale Konstante von Fall 5 muss im RAM liegen (ein interessanter Fall, der jedoch genau mit Fall 3 identisch ist).
  • Die statische Konstante von Case6 muss sich im ROM befinden . Im Segment .rodata

Schauen wir uns nun die Debugging-Informationen und die generierte Map-Datei an. Der Debugger zeigt an, an welchen Adressen sich diese Konstanten befinden.

Bild

Wie ich bereits sagte, sind die Adressen 0x0800 ... das sind ROM- Adressen und 0x200 ... das sind RAM . Mal sehen, in welchen Segmenten der Compiler diese Konstanten verteilt hat:

  .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 

Vier globale und statische Konstanten fielen in das .rodata- Segment, und zwei lokale Variablen fielen nicht in die Map-Datei, da sie auf dem Stapel erstellt wurden und ihre Adresse den Adressen des Stapels entspricht. Das CSTACK-Segment beginnt bei 0x2000'2488 und endet bei 0x2000'0488. Wie Sie auf dem Bild sehen können, werden die Konstanten erst am Anfang des Stapels erstellt.

Der Compiler platziert globale und statische Konstanten im .rodata- Segment, deren Position in den Linker-Einstellungen angegeben ist.

Ein weiterer wichtiger Punkt ist die Initialisierung . Globale und statische Variablen, einschließlich Konstanten, müssen initialisiert werden. Dies kann auf verschiedene Arten erfolgen. Wenn es sich um eine Konstante handelt, die im .rodata- Segment liegt, erfolgt die Initialisierung in der Kompilierungsphase, d. H. Der Wert wird sofort an die Adresse geschrieben, an der sich die Konstante befindet. Wenn dies eine reguläre Variable ist, kann die Initialisierung erfolgen, indem der Wert aus dem ROM-Speicher in die Adresse der globalen Variablen kopiert wird:

Wenn beispielsweise die globale Variable int i = 3 definiert ist, hat der Compiler sie im Daten-Datensegment definiert. Der Linker hat sie auf 0x20000000 gesetzt:
.data inited 0x2000'0000 ,
und sein Initialisierungswert (3) liegt im .rodata- Segment unter der Adresse 0x8000190:
Initializer bytes const 0x800'0190
Wenn Sie diesen Code schreiben:

 int i = 3; const int c = i; 

Es ist offensichtlich, dass die globale Konstante erst initialisiert wird, nachdem die globale Variable i initialisiert wurde, d. H. Zur Laufzeit. In diesem Fall befindet sich die Konstante im RAM

Nun, wenn wir zu unserem zurückkehren
erstes Beispiel
 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 ; } 

Und wir fragen uns: In welchem ​​Segment hat der Compiler das konstante Objekt myConstInROM ? Und wir bekommen die Antwort: Die Konstante liegt im .bss- Segment und enthält statische und globale Variablen, die auf Null (0) initialisiert sind.
.bss inited 0x2000'0004 0x4
myConstInROM 0x2000'0004 0x4


Warum? Denn in C ++ befindet sich ein Datenobjekt, das als Konstante deklariert ist und eine dynamische Initialisierung benötigt, im Lese- / Schreibspeicher und wird beim Erstellen initialisiert.

In diesem Fall erfolgt eine dynamische Initialisierung, const WantToBeInROM myConstInROM(10) , und der Compiler fügt dieses Objekt in das .bss- Segment ein, initialisiert zuerst alle Felder 0 und ruft dann beim Erstellen eines konstanten Objekts den Konstruktor auf, um das Feld i Wert 10 zu initialisieren.

Wie können wir den Compiler dazu bringen, unser Objekt im .rodata- Segment zu platzieren? Die Antwort auf diese Frage ist einfach. Sie sollten immer eine statische Initialisierung durchführen. Sie können es so machen:

1. In unserem Beispiel ist zu sehen, dass der Compiler die dynamische Initialisierung im Prinzip auf statisch optimieren kann, da der Konstruktor recht einfach ist. Für die IAR des Compilers können Sie die Konstante mit dem Attribut __ro_placement markieren
__ro_placement const WantToBeInROM myConstInROM
Mit dieser Option platziert der Compiler die Variable an der Adresse im ROM:
myConstInROM 0x800'0144 0x4 Data
Offensichtlich ist dieser Ansatz nicht universell und im Allgemeinen sehr spezifisch. Deshalb gehen wir zur richtigen Methode über :)

2. Es ist ein constexpr Konstruktor zu constexpr . Wir weisen den Compiler sofort an, die statische Initialisierung zu verwenden, d. H. In der Kompilierungsphase wird das gesamte Objekt im Voraus vollständig "berechnet" und alle seine Felder sind bekannt. Alles was wir tun müssen, ist constexpr zum Konstruktor hinzuzufügen.

Objekt fliegt zum 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 ; } 


Um sicherzugehen, dass sich Ihr konstantes Objekt im ROM befindet, müssen Sie einfache Regeln befolgen:
  1. Das .text- Segment, in dem der Code platziert ist, sollte sich im ROM befinden. Es wird in den Linker-Einstellungen konfiguriert.
  2. Das .rodata- Segment, in dem globale und statische Konstanten platziert sind, muss sich im ROM befinden. Es wird in den Linker-Einstellungen konfiguriert.
  3. Die Konstante muss global oder statisch sein.
  4. Attribute einer konstanten Variablenklasse dürfen nicht veränderbar sein
  5. Die Initialisierung des Objekts muss statisch sein, d. H. Der Konstruktor der Klasse, dessen Objekt eine Konstante sein wird, muss konstexpr sein oder überhaupt nicht definiert sein (es gibt keine dynamische Initialisierung).
  6. Wenn möglich, wenn Sie sicher sind, dass das Objekt im ROM anstelle von const gespeichert werden soll, verwenden Sie constexpr

Ein paar Worte zum constexpr und zum constexpr-Konstruktor. Der Hauptunterschied zwischen const und constexpr besteht darin, dass die Initialisierung der const-Variablen bis zur Laufzeit verzögert werden kann. Die Variable constexpr muss zur Kompilierungszeit initialisiert werden.
Alle constexpr-Variablen sind vom Typ const.

Die Definition des constexpr-Konstruktors muss die folgenden Anforderungen erfüllen:
  • Eine Klasse darf keine virtuellen Basisklassen haben.
     struct D2 : virtual BASE { //error, D2 must not have virtual base class. constexpr D2() : BASE(), mem(55) { } private: int mem; }; 
  • Jeder der Parametertypen der Klasse muss ein Literaltyp sein.
  • Der Konstruktorkörper muss = delete oder = default . Oder erfüllen Sie die folgenden Anforderungen:
  • Es gibt keine try catch Blöcke im Konstruktorkörper.
  • Der Konstruktorkörper kann nullptr
  • Der Konstruktorkörper kann static_assert
  • Im Hauptteil des Konstruktors kann typedef verwendet werden, das keine Klassen oder Aufzählungen definiert
  • Der Konstruktorkörper kann Direktiven und Deklarationen using
  • Jedes nicht statische Mitglied einer Klasse oder Basisklasse muss initialisiert werden.
  • Konstruktoren einer Klasse oder Basisklasse, die zum Initialisieren nicht statischer Elemente von Klassenmitgliedern und Unterobjekten der Basisklasse verwendet werden, müssen constexpr .
  • Initialisierer für alle nicht statischen Datenelemente müssen constexpr
  • Bei der Initialisierung von Klassenmitgliedern müssen alle Typkonvertierungen in einem konstanten Ausdruck gültig sein. Die Verwendung von reinterpret_cast und das Umwandeln von void* in einen anderen Zeigertyp ist beispielsweise nicht zulässig

Der implizite Standardkonstruktor ist der constexpr-Konstruktor. Schauen wir uns nun einige Beispiele an:

Beispiel 1. Objekt im 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; } 


Dies ist besser, nicht zu schreiben, da das Objekt in den RAM fliegt, sobald Sie sich entscheiden, das Attribut i zu initialisieren

Beispiel 2. Ein Objekt im 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; } 



Beispiel 3. Ein Objekt im 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; } 



Beispiel 4. Objekt im 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; } 



Beispiel 5. Ein Objekt im 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; } 



Beispiel 6. Objekt im 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; } 



Beispiel 7. Kompilierungsfehler
 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; } 



Beispiel 8. Ein Objekt im ROM, das von einer abstrakten Klasse erbt
 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; } 



Beispiel 9. Ein Objekt im ROM aggregiert ein Objekt im 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; } 



Beispiel 10. Das gleiche, aber statische Objekt im 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; } 



Beispiel 11. Und jetzt ist das konstante Objekt nicht statisch und daher im 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; } 



Beispiel 12. Kompilierungsfehler.
 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; } 



Beispiel 13. Kompilierungsfehler
 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; } 



Beispiel 14. Ein Objekt im 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; } 



Und schließlich ein konstantes Objekt, das ein Array enthält, mit Array-Initialisierung über eine constexpr-Funktion.
 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; } 


Referenzen:
IAR C / C ++ - Entwicklungshandbuch
Constexpr-Konstruktoren (C ++ 11)
constexpr (C ++)

PS.
Nach einer sehr nützlichen Diskussion mit Valdaros müssen Sie die folgenden Punkttangenskonstanten hinzufügen. Gemäß dem C ++ - Standard und diesem Dokument N1076.pdf

1. Jede Änderung an einem konstanten Objekt (mit Ausnahme von veränderlichen Mitgliedern einer Klasse) während seiner Lebensdauer führt zu einem undefinierten Verhalten. Das heißt,

  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. Das Problem ist, dass dies nur während der gesamten Lebensdauer eines konstanten Objekts funktioniert, im Konstruktor und Destruktor jedoch nicht. Daher ist es durchaus legitim, dies zu tun:

 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; } 

Und es gilt als legal. Trotz der Tatsache, dass wir den Konstruktor constexpr und die darin enthaltene Funktion constexpr verwendet haben. Das Objekt geht direkt in den RAM.

Um dies zu vermeiden, verwenden Sie const - constexpr anstelle von const. Dann tritt ein Kompilierungsfehler auf, der Sie darüber informiert, dass etwas nicht stimmt und das Objekt nicht konstant sein kann.

 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/de453262/


All Articles