¿Dónde se almacenan sus constantes en el microcontrolador CortexM (usando el compilador IAR de C ++ como ejemplo)

Enseño a mis alumnos cómo trabajar con el microcontrolador STM32F411RE, a bordo del cual hay hasta 512 kB de ROM y 128 kB de RAM
Por lo general, un programa se escribe en la ROM en este microcontrolador, y los datos variables a menudo se necesitan en la RAM para que las constantes estén en la ROM .
En el microcontrolador ROM STM32F411RE, la memoria se encuentra en direcciones con 0x08000000 ... 0x0807FFFF y RAM con 0x20000000 ... 0x2001FFFF.

Y si todas las configuraciones del enlazador son correctas, el alumno calcula que en un código tan directo su constante reside en 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 ; } 

También puede intentar responder la pregunta: ¿dónde está la constante myConstInROM en ROM o en RAM ?

Si respondiste a esta pregunta que en ROM , te felicito, de hecho lo más probable es que estés equivocado, la constante generalmente estará en RAM y para descubrir cómo colocar correcta y correctamente tus constantes en ROM , bienvenido a cat.

Introduccion


Primero, una pequeña digresión, ¿por qué preocuparse por esto?
Al desarrollar software crítico de seguridad para dispositivos de medición que cumplan con IEC 61508-3: 2010 o un equivalente doméstico de GOST IEC 61508-3-2018 , se deben tener en cuenta varios puntos que no son críticos para el software convencional.

El mensaje principal de este estándar es que el software debe detectar cualquier falla que afecte la confiabilidad del sistema y poner el sistema en modo de "bloqueo"

Además de las fallas mecánicas obvias, por ejemplo, falla del sensor o degradación y falla de los componentes electrónicos, se deben detectar los errores causados ​​por la falla del entorno del software, por ejemplo, el microcontrolador RAM o ROM .

Y si en los primeros dos casos, es posible detectar un error solo de manera indirecta bastante confusa (hay algoritmos que determinan la falla del sensor, por ejemplo, el Método de evaluación del estado de un convertidor térmico de resistencia ), entonces, en el caso de una falla del entorno del software, esto puede hacerse mucho más fácilmente, por ejemplo, una falla de memoria puede verificar mediante una simple verificación de integridad de datos. Si se viola la integridad de los datos, entonces interprete esto como un fallo de memoria.

Si los datos permanecen durante mucho tiempo en la RAM sin verificar y actualizar, entonces la probabilidad de que algo les suceda debido a una falla de RAM aumenta con el tiempo. Un ejemplo son algunos coeficientes de calibración para calcular la temperatura que se estableció en la fábrica y se escribió en una EEPROM externa, al inicio se leen y se escriben en la RAM y están allí hasta que se apaga la alimentación. Y en la vida, el sensor de temperatura puede funcionar todo el período del intervalo entre calibraciones, hasta 3-5 años. Obviamente, dichos datos de RAM deben protegerse y verificarse periódicamente para verificar su integridad.

Pero también hay datos, como una constante declarada simplemente para facilitar la lectura, un objeto de un controlador LCD, SPI o I2C, que no debe cambiarse, se crean una vez y no se eliminan hasta que se apaga la alimentación.

Estos datos se guardan mejor en ROM . Es más confiable desde el punto de vista de la tecnología y es mucho más fácil verificarlo, es suficiente leer periódicamente la suma de verificación de toda la memoria de solo lectura en alguna tarea de baja prioridad. Si la suma de comprobación no coincide, simplemente puede informar la falla de la ROM y el sistema de diagnóstico mostrará un accidente.

Si estos datos se encuentran en la RAM , sería problemático o incluso imposible determinar su integridad debido al hecho de que no está claro dónde están los datos inmutables en la RAM y dónde son mutables, el enlazador lo coloca como quiere, y para proteger cada objeto de RAM con una suma de verificación parece paranoia

Por lo tanto, la forma más fácil es estar 100% seguro de que los datos constantes están en la ROM . Cómo hacer esto, quiero intentar explicarlo. Pero primero debes hablar sobre la organización de la memoria en ARM.

Organización de la memoria


Como saben, el núcleo ARM tiene una arquitectura de Harvard: los buses de datos y códigos están separados. Por lo general, esto significa que se supone que hay una memoria separada para los programas y una memoria separada para los datos. Pero el hecho es que ARM es una arquitectura de Harvard modificada, es decir. el acceso a la memoria se lleva a cabo en un bus, y el dispositivo de administración de memoria ya proporciona la separación de los buses mediante señales de control: leer, escribir o seleccionar un área de memoria.

Por lo tanto, los datos y el código pueden estar en la misma área de memoria. En este único espacio de direcciones se puede ubicar y memoria ROM y RAM y periféricos. Y esto significa que, de hecho, tanto el código como los datos pueden llegar incluso donde depende del compilador y el vinculador.

Por lo tanto, para distinguir entre las áreas de memoria para ROM (Flash) y RAM, generalmente se indican en la configuración del vinculador, por ejemplo, en IAR 8.40.1, se ve así:

 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 en este microcontrolador se encuentra en 0x20000000 ... 0x2001FFF, y la ROM en 0x008000000 ... 0x0807FFFF .
Puede cambiar fácilmente la dirección inicial ROM_start a la dirección RAM, digamos RAM_start y la dirección final ROM_end__ a RAM_end__ y su programa estará completamente ubicado en la RAM.
Incluso puede hacer lo contrario y especificar RAM en el área de memoria ROM , y su programa se ensamblará y parpadeará correctamente, aunque no funcionará :)
Algunos microcontroladores, como AVR, inicialmente tienen un espacio de direcciones separado para la memoria del programa, la memoria de datos y los periféricos, y por lo tanto, tales trucos no funcionarán allí, y el programa se escribe en la ROM de forma predeterminada.

Todo el espacio de direcciones en CortexM es único, y el código y los datos se pueden ubicar en cualquier lugar. Usando la configuración del enlazador, puede establecer la región para las direcciones ROM y RAM . IAR localiza el segmento de código .text en la región ROM

Archivo de objetos y segmentos


Arriba, mencioné el segmento de código, veamos de qué se trata.

Se crea un archivo de objeto separado para cada módulo compilado, que contiene la siguiente información:

  • Código y segmentos de datos
  • Información de depuración enana
  • Tabla de personajes

Estamos interesados ​​en segmentos de código y datos. Un segmento es un elemento que contiene un fragmento de código o datos que deben colocarse en una dirección física en la memoria. Un segmento puede contener varios fragmentos, generalmente un fragmento para cada variable o función. Se puede colocar un segmento en ROM y RAM .
Cada segmento tiene un nombre y un atributo que define su contenido. El atributo se usa para definir un segmento en la configuración del enlazador. Por ejemplo, los atributos pueden ser:

  • código - código ejecutable
  • solo lectura - variables constantes
  • readwrite - variables inicializadas
  • zeroinit - variables inicializadas cero

Por supuesto, hay otros tipos de segmentos, por ejemplo, segmentos que contienen información de depuración, pero solo nos interesarán aquellos que contengan código o datos de nuestra aplicación.

En general, un segmento es el bloque enlazable más pequeño. Sin embargo, si es necesario, el enlazador también puede indicar bloques (fragmentos) aún más pequeños. No consideraremos esta opción, haremos con segmentos.

Durante la compilación, los datos y las funciones se colocan en diferentes segmentos. Y durante la vinculación, el vinculador asigna direcciones físicas reales a diferentes segmentos. El compilador IAR tiene nombres de segmento predefinidos, algunos de los cuales proporcionaré a continuación:

  • .bss : contiene variables estáticas y globales inicializadas en 0
  • .CSTACK - Contiene la pila utilizada por el programa
  • .data - Contiene variables inicializadas estáticas y globales
  • .data_init : contiene los valores iniciales para los datos en la sección .data si se usa la directiva de inicialización para el enlazador
  • HEAP : contiene el montón utilizado para alojar datos dinámicos
  • .intvec - Contiene una tabla de vectores de interrupción
  • .rodata - Contiene datos constantes
  • .text - Contiene el código del programa

Para entender dónde se encuentran las constantes, solo nos interesarán los segmentos
.rodata : un segmento en el que se almacenan las constantes,
.data : un segmento en el que se almacenan todas las variables estáticas y globales inicializadas,
.bss : un segmento en el que se almacenan todas las variables .data estáticas y globales inicializadas con cero (0),
.text : un segmento para almacenar código.

En la práctica, esto significa que si define la variable int val = 3 , el compilador ubicará la variable en el segmento .data y se marcará con el atributo readwrite , y el número 3 se puede colocar en el segmento .text o en el segmento .rodata o, si Se aplica una directiva especial para el vinculador en .data_init y también se marca como de solo lectura .

El segmento .rodata contiene datos constantes e incluye variables constantes, cadenas, literales agregados, etc. Y este segmento se puede colocar en cualquier lugar de la memoria.

Ahora queda más claro lo que se prescribe en la configuración del vinculador y por qué:

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

Es decir, todos los datos marcados con el atributo de solo lectura se deben colocar en ROM_region. Por lo tanto, los datos de diferentes segmentos, pero marcados con el atributo de solo lectura, pueden ingresar a la ROM.

Bueno, eso significa que todas las constantes deben estar en la ROM, pero ¿por qué en nuestro código, al comienzo del artículo, el objeto constante todavía se encuentra en 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 ; } 



Datos constantes


Antes de aclarar la situación, recordemos primero que las variables globales se crean en la memoria compartida, las variables locales, es decir Las variables declaradas dentro de las funciones "normales" se crean en la pila o en los registros, y las variables locales estáticas también se crean en la memoria compartida.

¿Qué significa esto en C ++? Veamos un ejemplo:

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

Todos estos son datos constantes. Pero para cualquiera de ellos se aplica la regla de creación descrita anteriormente, las variables locales se crean en la pila. Por lo tanto, con nuestra configuración de enlazador, debería ser así:

  • La constante global Case1 debe estar en ROM . En el segmento .rodata
  • La constante global de Case2 debe estar en ROM . En el segmento .rodata
  • La constante local de Case3 debe estar en la RAM (la constante se creó en la pila en el segmento STACK)
  • La constante estática de Case4 debe estar en ROM . En el segmento .rodata
  • La constante local de Case5 debe estar en la RAM (un caso interesante, pero es exactamente idéntico al caso 3.)
  • La constante estática de Case6 debe estar en ROM . En el segmento .rodata

Ahora veamos la información de depuración y el archivo de mapa generado. El depurador muestra en qué direcciones se encuentran estas constantes.

imagen

Como dije antes, las direcciones 0x0800 ... estas son direcciones ROM , y 0x200 ... estas son RAM . Veamos en qué segmentos el compilador distribuyó estas 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 

Cuatro constantes globales y estáticas cayeron en el segmento .rodata , y dos variables locales no cayeron en el archivo de mapa porque se crean en la pila y su dirección corresponde a las direcciones de la pila. El segmento CSTACK comienza en 0x2000'2488 y termina en 0x2000'0488. Como puede ver en la imagen, las constantes se crean al principio de la pila.

El compilador coloca constantes globales y estáticas en el segmento .rodata , cuya ubicación se especifica en la configuración del vinculador.

Vale la pena señalar otro punto importante, la inicialización . Las variables globales y estáticas, incluidas las constantes, deben inicializarse. Y esto se puede hacer de varias maneras. Si es una constante que se encuentra en el segmento .rodata , la inicialización ocurre en la etapa de compilación, es decir. el valor se escribe inmediatamente en la dirección donde se encuentra la constante. Si esta es una variable regular, entonces la inicialización puede ocurrir copiando el valor de la memoria ROM a la dirección de la variable global:

Por ejemplo, si la variable global int i = 3 definida, entonces el compilador la definió en el segmento de datos .data , el enlazador la puso en 0x20000000:
.data inited 0x2000'0000 ,
y su valor de inicialización (3) se ubicará en el segmento .rodata en la dirección 0x8000190:
Initializer bytes const 0x800'0190
Si escribes este código:

 int i = 3; const int c = i; 

Es obvio que la constante global , se inicializa solo después de que se inicializa la variable global i , es decir, en tiempo de ejecución. En este caso, la constante se ubicará en la RAM

Ahora si volvemos a nuestro
ejemplo inicial
 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 ; } 

Y nos preguntamos: ¿en qué segmento definió el compilador el objeto constante myConstInROM ? Y obtenemos la respuesta: la constante se ubicará en el segmento .bss, que contiene variables estáticas y globales inicializadas en cero (0).
.bss inited 0x2000'0004 0x4
myConstInROM 0x2000'0004 0x4


Por qué Porque en C ++, un objeto de datos que se declara como una constante y que necesita inicialización dinámica se encuentra en la memoria de lectura y escritura y se inicializará en el momento de la creación.

En este caso, se produce una inicialización dinámica, const WantToBeInROM myConstInROM(10) , y el compilador coloca este objeto en el segmento .bss , inicializando todos los campos 0 primero, y luego, al crear un objeto constante, llama al constructor para inicializar el campo i valor 10.

¿Cómo podemos hacer que el compilador coloque nuestro objeto en el segmento .rodata ? La respuesta a esta pregunta es simple, siempre debe realizar una inicialización estática. Puedes hacerlo de esta manera:

1. En nuestro ejemplo, se puede ver que, en principio, el compilador puede optimizar la inicialización dinámica en estática, ya que el constructor es bastante simple. Para el IAR del compilador, puede marcar la constante con el atributo __ro_placement
__ro_placement const WantToBeInROM myConstInROM
Con esta opción, el compilador colocará la variable en la dirección en ROM:
myConstInROM 0x800'0144 0x4 Data
Obviamente, este enfoque no es universal y generalmente es muy específico. Por lo tanto, pasamos al método correcto :)

2. Es hacer un constructor constexpr . Inmediatamente le decimos al compilador que use la inicialización estática, es decir. en la etapa de compilación, cuando todo el objeto será completamente "calculado" de antemano y todos sus campos serán conocidos. Todo lo que necesitamos hacer es agregar constexpr al constructor.

El objeto vuela a 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 ; } 


Por lo tanto, para asegurarse de que su objeto constante esté en ROM, debe seguir reglas simples:
  1. El segmento .text en el que se coloca el código debe estar en la ROM. Está configurado en la configuración del vinculador.
  2. El segmento .rodata en el que se colocan las constantes globales y estáticas debe estar en la ROM. Está configurado en la configuración del vinculador.
  3. La constante debe ser global o estática.
  4. Los atributos de una clase de variable constante no deben ser mutables
  5. La inicialización del objeto debe ser estática, es decir, el constructor de la clase cuyo objeto será una constante debe ser constexpr o no estar definido en absoluto (no hay inicialización dinámica)
  6. Si es posible, si está seguro de que el objeto debe almacenarse en la ROM en lugar de const, use constexpr

Algunas palabras sobre el constexpr y el constructor constexpr. La principal diferencia entre const y constexpr es que la inicialización de la variable const puede retrasarse hasta el tiempo de ejecución. La variable constexpr debe inicializarse en tiempo de compilación.
Todas las variables constexpr son de tipo const.

La definición de constructor constexpr debe cumplir los siguientes requisitos:
  • Una clase no debe tener clases base virtuales.
     struct D2 : virtual BASE { //error, D2 must not have virtual base class. constexpr D2() : BASE(), mem(55) { } private: int mem; }; 
  • Cada uno de los tipos de parámetros de la clase debe ser un tipo literal.
  • El cuerpo del constructor debe ser = delete o = default . O cumpla los requisitos a continuación:
  • No hay bloques try catch en el cuerpo del constructor.
  • El cuerpo del constructor puede usar nullptr
  • El cuerpo del constructor puede usar static_assert
  • En el cuerpo del constructor, se puede usar typedef que no define clases o enumeraciones
  • El cuerpo del constructor puede usar directivas y declaraciones using
  • Cada miembro no estático de una clase o clase base debe inicializarse.
  • Los constructores de una clase o clase base, utilizados para inicializar elementos no estáticos de miembros de clase y subobjetos de la clase base, deben ser constexpr .
  • Los inicializadores para todos los elementos de datos no estáticos deben ser constexpr
  • Al inicializar miembros de clase, todas las conversiones de tipo deben ser válidas en una expresión constante. Por ejemplo, no se permite el uso de reinterpret_cast y la conversión de void* a otro tipo de puntero

El constructor implícito predeterminado es el constructor constexpr. Ahora veamos algunos ejemplos:

Ejemplo 1. Objeto en 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; } 


Es mejor no escribir de esta manera, porque tan pronto como decidas inicializar el atributo i, el objeto volará a la RAM

Ejemplo 2. Un objeto 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; } 



Ejemplo 3. Un objeto 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; } 



Ejemplo 4. Objeto en 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; } 



Ejemplo 5. Un objeto 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; } 



Ejemplo 6. Objeto en 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; } 



Ejemplo 7. Error de compilación
 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; } 



Ejemplo 8. Un objeto en ROM, heredado de una clase abstracta
 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; } 



Ejemplo 9. Un objeto en ROM agrega un objeto ubicado 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; } } ; 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; } 



Ejemplo 10. El mismo objeto pero estático en 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; } 



Ejemplo 11. Y ahora el objeto constante no es estático y, por lo tanto, está 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; } 



Ejemplo 12. Error de compilación.
 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; } 



Ejemplo 13. Error de compilación
 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; } 



Ejemplo 14. Un objeto en 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; } 



Y finalmente, un objeto constante que contiene una matriz, con inicialización de matriz a través de una función 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; } 


Referencias
Guía de desarrollo IAR C / C ++
Constexpr constructores (C ++ 11)
constexpr (C ++)

PS.
Después de una discusión muy útil con Valdaros, debe agregar las siguientes constantes tangentes de puntos. De acuerdo con el estándar C ++ y este documento N1076.pdf

1. Cualquier cambio a un objeto constante (con la excepción de miembros mutables de una clase) durante su vida útil conduce a un Comportamiento indefinido. Es decir

  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. El problema es que esto solo funciona durante toda la vida de un objeto constante, pero en el constructor y destructor no funciona. Por lo tanto, es bastante legítimo hacerlo:

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

Y se considera legal. A pesar de que usamos el constructor constexpr, y la función constexpr en él. El objeto va directamente a la RAM.

Para evitar esto, use const - constexpr en lugar de const, luego habrá un error de compilación que le indicará que algo está mal y que el objeto no puede ser constante.

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


All Articles