Onde estão suas constantes armazenadas no microcontrolador CortexM (usando o compilador C ++ IAR como exemplo)

Ensino meus alunos como usar o microcontrolador STM32F411RE, no qual existem 512 kB de ROM e 128 kB de RAM
Geralmente neste microcontrolador, um programa é gravado na memória ROM e, na RAM, os dados a serem alterados geralmente são necessários para criar constantes na ROM .
No microcontrolador STM32F411RE, ROM , a memória está localizada em endereços com 0x08000000 ... 0x0807FFFF e RAM com 0x20000000 ... 0x2001FFFF.

E se todas as configurações do vinculador estiverem corretas, o aluno calcula que, em um código tão simples, sua constante está na 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 ; } 

Você também pode tentar responder à pergunta: onde está o myConstInROM constante na ROM ou na RAM ?

Se você respondeu a essa pergunta que na ROM , eu o parabenizo, na verdade provavelmente você está errado, a constante geralmente está na RAM e para descobrir como colocar correta e corretamente suas constantes na ROM - bem-vindo ao gato.

1. Introdução


Primeiro, uma pequena digressão, por que se preocupar com isso?
Ao desenvolver software crítico de segurança para dispositivos de medição que estejam em conformidade com a IEC 61508-3: 2010 ou um equivalente doméstico da GOST IEC 61508-3-2018 , vários pontos devem ser levados em consideração que não são críticos para o software convencional.

A principal mensagem deste padrão é que o software deve detectar qualquer falha que afete a confiabilidade do sistema e colocá-lo no modo "travar"

Além de falhas mecânicas óbvias, por exemplo, falha no sensor ou degradação e falha de componentes eletrônicos, erros causados ​​pela falha do ambiente de software, por exemplo, microcontrolador de RAM ou ROM , devem ser detectados.

E se nos dois primeiros casos, é possível detectar um erro apenas de maneiras indiretas um pouco confusas (existem algoritmos que determinam a falha do sensor, por exemplo, o Método de avaliação do estado de um conversor térmico de resistência ), então, no caso de uma falha no ambiente de software, isso pode ser feito com muito mais facilidade, por exemplo, uma falha de memória pode verifique com uma simples verificação de integridade dos dados. Se a integridade dos dados for violada, interprete isso como uma falha de memória.

Se os dados estiverem na RAM por um longo período sem verificação e atualização, a probabilidade de que algo lhes aconteça devido à falha da RAM se tornará mais alta com o tempo. Um exemplo são alguns coeficientes de calibração para calcular a temperatura definida na fábrica e gravada em uma EEPROM externa. Na inicialização, elas são lidas e gravadas na RAM e ficam lá até que a energia seja desligada. E na vida, o sensor de temperatura pode trabalhar durante todo o período do intervalo de calibração, até 3-5 anos. Obviamente, esses dados de RAM devem ser protegidos e verificados periodicamente quanto à sua integridade.

Mas também existem dados, como uma constante declarada simplesmente para facilitar a leitura, um objeto de um driver de LCD, SPI ou I2C, que não deve ser alterado, é criado uma vez e não é excluído até que a energia seja desligada.

Esses dados são mais bem guardados na ROM . É mais confiável do ponto de vista da tecnologia e é muito mais fácil verificá-la, basta ler periodicamente a soma de verificação de toda a memória somente leitura em alguma tarefa de baixa prioridade. Se a soma de verificação não corresponder, você pode simplesmente relatar a falha da ROM e o sistema de diagnóstico exibirá um acidente.

Se esses dados estiverem na RAM , seria problemático ou até impossível determinar sua integridade devido ao fato de não estar claro onde os dados imutáveis ​​estão na RAM e onde são mutáveis, o vinculador o coloca como deseja e para proteger cada objeto de RAM com uma soma de verificação paranóia.

Portanto, a maneira mais fácil é ter 100% de certeza de que os dados constantes estão na ROM . Como fazer isso, eu quero tentar explicar. Mas primeiro você precisa falar sobre a organização da memória no ARM.

Organização da memória


Como você sabe, o núcleo do ARM possui uma arquitetura de Harvard - os barramentos de dados e código são separados. Normalmente, isso significa que se supõe que haja uma memória separada para programas e uma memória separada para dados. Mas o fato é que o ARM é uma arquitetura de Harvard modificada, ou seja, o acesso à memória é realizado em um barramento e o dispositivo de gerenciamento de memória já fornece a separação de barramentos usando sinais de controle: leia, escreva ou selecione uma área de memória.

Assim, dados e código podem estar na mesma área de memória. Neste espaço de endereço único pode ser localizado e memória ROM e RAM e periféricos. E isso significa que, de fato, tanto o código quanto os dados podem chegar até onde depende do compilador e vinculador.

Portanto, para distinguir entre as áreas de memória da ROM (Flash) e RAM, elas geralmente são indicadas nas configurações do vinculador, por exemplo, na IAR 8.40.1, é assim:

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

A RAM neste microcontrolador está localizada em 0x20000000 ... 0x2001FFF e a ROM em 0x008000000 ... 0x0807FFFF .
Você pode alterar facilmente o endereço inicial ROM_start para o endereço RAM, digamos RAM_start e o endereço final ROM_end__ para RAM_end__, e seu programa estará completamente localizado na RAM.
Você pode até fazer o oposto e especificar a RAM na área de memória da ROM , e seu programa será montado e piscará com sucesso, embora não funcione :)
Alguns microcontroladores, como o AVR, inicialmente possuem um espaço de endereço separado para a memória do programa, a memória de dados e os periféricos e, portanto, esses truques não funcionam lá, e o programa é gravado na ROM por padrão.

Todo o espaço de endereço no CortexM é único e o código e os dados podem ser localizados em qualquer lugar. Usando as configurações do vinculador, você pode definir a região para os endereços ROM e RAM . O IAR localiza o segmento de código .text na região da ROM

Arquivo e segmentos de objetos


Acima, mencionei o segmento de código, vamos ver o que é.

Um arquivo de objeto separado é criado para cada módulo compilado, que contém as seguintes informações:

  • Segmentos de código e dados
  • Informações sobre depuração do DWARF
  • Tabela de caracteres

Estamos interessados ​​em segmentos de código e dados. Um segmento é um elemento que contém um pedaço de código ou dados que deve ser colocado em um endereço físico na memória. Um segmento pode conter vários fragmentos, geralmente um fragmento para cada variável ou função. Um segmento pode ser colocado na ROM e na RAM .
Cada segmento tem um nome e um atributo que define seu conteúdo. O atributo é usado para definir um segmento na configuração do vinculador. Por exemplo, os atributos podem ser:

  • código - código executável
  • readonly - variáveis ​​constantes
  • readwrite - variáveis ​​inicializadas
  • zeroinit - variáveis ​​inicializadas com zero

Obviamente, existem outros tipos de segmentos, por exemplo, segmentos contendo informações de depuração, mas estaremos interessados ​​apenas naqueles que contêm código ou dados de nosso aplicativo.

Em geral, um segmento é o menor bloco vinculável. No entanto, se necessário, o vinculador também pode indicar blocos ainda menores (fragmentos). Não consideraremos essa opção, faremos com segmentos.

Durante a compilação, dados e funções são colocados em diferentes segmentos. E durante a vinculação, o vinculador atribui endereços físicos reais a diferentes segmentos. O compilador IAR possui nomes de segmentos predefinidos, alguns dos quais fornecerei abaixo:

  • .bss - Contém variáveis ​​estáticas e globais inicializadas em 0
  • .CSTACK - Contém a pilha usada pelo programa
  • .data - Contém variáveis ​​inicializadas estáticas e globais
  • .data_init - contém os valores iniciais dos dados na seção .data se a diretiva de inicialização do vinculador for usada
  • HEAP - contém a pilha usada para hospedar dados dinâmicos
  • .intvec - contém uma tabela de vetores de interrupção
  • .rodata - Contém dados constantes
  • .text - Contém código do programa

Para entender onde as constantes estão localizadas, estaremos interessados ​​apenas em segmentos
.rodata - um segmento no qual constantes são armazenadas,
.data - um segmento no qual todas as variáveis ​​estáticas e globais inicializadas são armazenadas,
.bss - um segmento no qual todas as variáveis ​​estáticas e globais de dados inicializadas com zero (0) são armazenadas,
.text - um segmento para armazenar código.

Na prática, isso significa que se você definir a variável int val = 3 , a variável em si será localizada pelo compilador no segmento .data e marcada com o atributo readwrite , e o número 3 poderá ser colocado no segmento .text ou no segmento .rodata ou, se uma diretiva especial para o vinculador em .data_init é aplicada e também é marcada como somente leitura por ele .

O segmento .rodata contém dados constantes e inclui variáveis ​​constantes, seqüências de caracteres, literais agregados e assim por diante. E esse segmento pode ser colocado em qualquer lugar da memória.

Agora fica mais claro o que é prescrito nas configurações do vinculador e por que:

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

Ou seja, todos os dados marcados com o atributo somente leitura devem ser colocados em ROM_region. Assim, dados de diferentes segmentos, mas marcados com o atributo readonly, podem entrar na ROM.

Bem, isso significa que todas as constantes devem estar na ROM, mas por que, no nosso código, no começo do artigo, o objeto constante ainda está na 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 ; } 



Dados constantes


Antes de esclarecer a situação, lembremos primeiro que variáveis ​​globais são criadas na memória compartilhada, variáveis ​​locais, ou seja, variáveis ​​declaradas dentro de funções "normais" são criadas na pilha ou nos registradores e variáveis ​​locais estáticas também são criadas na memória compartilhada.

O que isso significa em C ++. Vejamos um exemplo:

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

Isso tudo são dados constantes. Mas para qualquer um deles a regra de criação descrita acima se aplica, variáveis ​​locais são criadas na pilha. Portanto, com nossas configurações de vinculador, deve ser assim:

  • A constante global Case1 deve estar na ROM . No segmento .rodata
  • A constante global Case2 deve estar em ROM . No segmento .rodata
  • A constante local Case3 deve estar na RAM (a constante foi criada na pilha no segmento STACK)
  • A constante estática Case4 deve estar na ROM . No segmento .rodata
  • A constante local Case5 deve estar na RAM (um caso interessante, mas é exatamente idêntico ao Caso 3.)
  • A constante estática Case6 deve estar na ROM . No segmento .rodata

Agora vamos examinar as informações de depuração e o arquivo de mapa gerado. O depurador mostra em quais endereços essas constantes estão.

imagem

Como eu disse antes, os endereços 0x0800 ... são endereços ROM e 0x200 ... são RAM . Vamos ver em quais segmentos o compilador distribuiu essas 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 

Quatro constantes globais e estáticas caíram no segmento .rodata e duas variáveis ​​locais não caíram no arquivo de mapa porque são criadas na pilha e seu endereço corresponde aos endereços da pilha. O segmento CSTACK começa em 0x2000'2488 e termina em 0x2000'0488. Como você pode ver na figura, as constantes são criadas apenas no início da pilha.

O compilador coloca constantes globais e estáticas no segmento .rodata , cuja localização é especificada nas configurações do vinculador.

Vale a pena notar outro ponto importante, a inicialização . Variáveis ​​globais e estáticas, incluindo constantes, devem ser inicializadas. E isso pode ser feito de várias maneiras. Se houver uma constante no segmento .rodata , a inicialização ocorre no estágio de compilação, ou seja, o valor é gravado imediatamente no endereço em que a constante está localizada. Se essa for uma variável regular, a inicialização poderá ocorrer copiando o valor da memória ROM para o endereço da variável global:

Por exemplo, se a variável global int i = 3 definida, o compilador a definirá no segmento de dados .data , o vinculador o colocará em 0x20000000:
.data inited 0x2000'0000 ,
e seu valor de inicialização (3) estará no segmento .rodata no endereço 0x8000190:
Initializer bytes const 0x800'0190
Se você escrever este código:

 int i = 3; const int c = i; 

É óbvio que a constante global , é inicializada somente após a variável global i inicializada, ou seja, em tempo de execução. Nesse caso, a constante estará localizada na RAM

Agora, se voltarmos ao nosso
exemplo 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 ; } 

E nos perguntamos: em qual segmento o compilador definiu o objeto constante myConstInROM ? E obtemos a resposta: a constante estará no segmento .bss, contendo variáveis ​​estáticas e globais inicializadas em zero (0).
.bss inited 0x2000'0004 0x4
myConstInROM 0x2000'0004 0x4


Porque Como no C ++, um objeto de dados declarado como constante e que precisa de inicialização dinâmica está localizado na memória de leitura e gravação e será inicializado no momento da criação.

Nesse caso, a inicialização dinâmica ocorre, const WantToBeInROM myConstInROM(10) , e o compilador coloca esse objeto no segmento .bss , inicializando todos os campos 0 primeiro e, em seguida, ao criar um objeto constante, chamado construtor para inicializar o campo i valor 10.

Como podemos fazer o compilador colocar nosso objeto no segmento .rodata ? A resposta a esta pergunta é simples, você sempre deve executar a inicialização estática. Você pode fazer assim:

1. Em nosso exemplo, pode-se ver que, em princípio, o compilador pode otimizar a inicialização dinâmica para estática, uma vez que o construtor é bastante simples. Para o IAR do compilador, você pode marcar a constante com o atributo __ro_placement
__ro_placement const WantToBeInROM myConstInROM
Com esta opção, o compilador colocará a variável no endereço na ROM:
myConstInROM 0x800'0144 0x4 Data
Obviamente, essa abordagem não é universal e geralmente muito específica. Portanto, passamos ao método correto :)

2. É fazer um construtor constexpr . Dizemos imediatamente ao compilador para usar a inicialização estática, ou seja, no estágio de compilação, quando todo o objeto será "calculado" completamente com antecedência e todos os seus campos serão conhecidos. Tudo o que precisamos fazer é adicionar constexpr ao construtor.

O objeto voa para 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 ; } 


Portanto, para ter certeza de que seu objeto constante está na ROM, você precisa seguir regras simples:
  1. O segmento .text no qual o código é inserido deve estar na ROM. Está configurado nas configurações do vinculador.
  2. O segmento .rodata no qual as constantes globais e estáticas são colocadas deve estar na ROM. Está configurado nas configurações do vinculador.
  3. A constante deve ser global ou estática.
  4. Os atributos de uma classe de variável constante não devem ser mutáveis
  5. A inicialização do objeto deve ser estática, ou seja, o construtor da classe cujo objeto será uma constante deve ser consexpr ou não definido de forma alguma (não há inicialização dinâmica)
  6. Se possível, se você tiver certeza de que o objeto deve ser armazenado na ROM em vez de const, use constexpr

Algumas palavras sobre o constexpr e o construtor constexpr. A principal diferença entre const e constexpr é que a inicialização da variável const pode ser atrasada até o tempo de execução. A variável constexpr deve ser inicializada em tempo de compilação.
Todas as variáveis ​​constexpr são do tipo const.

A definição do construtor constexpr deve atender aos seguintes requisitos:
  • Uma classe não deve ter classes base virtuais.
     struct D2 : virtual BASE { //error, D2 must not have virtual base class. constexpr D2() : BASE(), mem(55) { } private: int mem; }; 
  • Cada um dos tipos de parâmetros da classe deve ser um tipo literal.
  • O corpo do construtor deve ser = delete ou = default . Ou satisfaça os requisitos abaixo:
  • Não há try catch blocos no corpo do construtor.
  • O corpo do construtor pode usar nullptr
  • O corpo do construtor pode usar static_assert
  • No corpo do construtor, typedef pode ser usado que não define classes ou enumerações
  • O corpo do construtor pode usar diretiva e declarações using
  • Cada membro não estático de uma classe ou classe base deve ser inicializado.
  • Os construtores de uma classe ou classe base, usados ​​para inicializar elementos não estáticos de membros e subobjetos da classe base, devem ser constexpr .
  • Inicializadores para todos os elementos de dados não estáticos devem ser constexpr
  • Ao inicializar os alunos, todas as conversões de tipo devem ser válidas em uma expressão constante. Por exemplo, não é permitido usar reinterpret_cast e converter de void* para outro tipo de ponteiro

O construtor padrão implícito é o construtor constexpr. Agora vamos ver alguns exemplos:

Exemplo 1. Objeto na 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; } 


É melhor não escrever dessa maneira, porque assim que você decidir inicializar o atributo i, o objeto voará para a RAM

Exemplo 2. Um objeto na 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; } 



Exemplo 3. Um objeto na 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; } 



Exemplo 4. Objeto na 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; } 



Exemplo 5. Um objeto na 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; } 



Exemplo 6. Objeto na 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; } 



Exemplo 7. Erro de compilação
 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; } 



Exemplo 8. Um objeto na ROM, herdado de uma classe abstrata
 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; } 



Exemplo 9. Um objeto na ROM agrega um objeto localizado na 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; } 



Exemplo 10. O mesmo objeto estático na 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; } 



Exemplo 11. E agora o objeto constante é não estático e, portanto, na 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; } 



Exemplo 12. Erro de compilação.
 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; } 



Exemplo 13. Erro de compilação
 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; } 



Exemplo 14. Um objeto na 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; } 



E, finalmente, um objeto constante contendo uma matriz, com a inicialização da matriz por meio de uma função 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; } 


Referências:
Guia de desenvolvimento do IAR C / C ++
construtores Constexpr (C ++ 11)
constexpr (C ++)

PS.
Após uma discussão muito útil com Valdaros, você precisa adicionar as seguintes constantes tangentes de ponto. De acordo com o padrão C ++ e este documento N1076.pdf

1. Qualquer alteração em um objeto constante (com exceção de membros mutáveis ​​de uma classe) durante sua vida útil leva ao comportamento indefinido. I.e.

  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. O problema é que isso só funciona durante toda a vida de um objeto constante, mas no construtor e destruidor não funciona. Portanto, é bastante legítimo fazê-lo:

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

E é considerado legal. Apesar de termos usado o construtor constexpr, e a função constexpr nele. O objeto vai direto para a RAM.

Para evitar isso, use const-constexpr em vez de const; haverá um erro de compilação que informará que algo está errado e que o objeto não pode 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/pt453262/


All Articles