Где хранятся ваши константы на микроконтроллере CortexM (на примере С++ IAR компилятора)

в 10:05, , рубрики: c++, const, constexpr, cortex-m, cortex-m4, микроконтроллеры, Программирование, программирование микроконтроллеров, С++

Я работаю со студентами на микроконтроллере STM32F411RE, имеющего целых 512 кБайт ROM и 128 кБайт ОЗУ.
Обычно в этом микроконтроллере в ROM память записывается программа, а в RAM изменяемые данные и очень часто нужно сделать так, чтобы константы лежали в ROM.
В микроконтроллере STM32F411RE, ROM память расположена по адресам с 0x08000000...0x0807FFFF, а RAM с 0x20000000...0x2001FFFF.

И если все настройки линкера правильные, студент рассчитывает, что вот в таком незамысловатом коде его константа лежит в ROM:

class WhantToBeInROM
{
private:
  int i;
public:
  WhantToBeInROM(int value): i(value) {}
  int Get() const
  {
    return i;
  }
};

const WhantToBeInROM myConstInROM(10);

int main()
{  
  std::cout << &myConstInROM << std::endl ;
}

Вы тоже можете пробовать ответить на вопрос: где лежит константа myConstInROM в ROM или в RAM?

Если вы ответили на этот вопрос, что в ROM, поздравляю вас, на самом деле скорее всего вы не правы, константа в общем случае будет лежать в RAM и чтобы разобраться, как правильно и законно расположить ваши константы в ROM — добро пожаловать под кат.

Введение

Вначале небольшое отступление, зачем вообще заморачиваться по этому поводу.
При разработке «safety critical» ПО для измерительных устройств, соответствующих стандарту IEC 61508-3:2010 или отечественного аналога ГОСТ IEC 61508-3-2018 приходится принимать во внимание ряд моментов, которые не являются критическими для обычного ПО.

Основной посыл этого стандарт заключается в том, что ПО должно обнаружить любой отказ, влияющий на надежность системы и перевести систему в режим «аварии»

Кроме очевидных механических поломок, например, вывода из строя или деградация сенсора и отказа электронных компонентов, должны выявляться ошибки, вызванные отказом окружением ПО, например, RAM или ROM микроконтроллера.

И если в первых двух случаях, обнаружить ошибку можно только довольно замороченными косвенными способами (есть алгоритмы, определяющие отказ сенсоров, например, Метод оценки состояния термопреобразователя сопротивления), то в случае отказа окружения ПО, это можно сделать намного проще, например, отказ памяти можно проверять, простой проверкой целостности данных. Если целостность данных нарушена, то интерпретировать это как отказ памяти.

Если данные долго лежат в RAM без проверки и обновления, то вероятность того, что с ними что-то случится из-за отказа RAM становится выше с течением времени. Примером могут служить какие-нибудь коэффициенты калибровки для расчета температуры, которые были заданы на производстве и записаны во внешнюю EEPROM, при запуске они считываются и записываются в RAM и находятся там до тех пор, пока питание не отключат. А в жизни, датчик температуры может работать весь период межповерочного интервала, до 3-5 лет. Очевидно, что такие RAM данные необходимо защищать и периодически проверять их целостность.

Но бывают и такие данные, как скажем просто объявленная для читабельности константа, объект драйвера LCD, SPI или I2C, которые не должны изменяться, создаются единожды и не удаляются до выключения питания.

Эти данные лучше держать в ROM. Она надежнее с точки зрения технологии и проверить её намного проще, достаточно, периодически считать контрольную сумму всей постоянной памяти в какой-нибудь низкоприоритетной задаче. При несовпадении контрольной суммы можно просто отрапортовать об отказе ROMи система диагностики выставит аварию.

Если бы эти данные лежали в RAM, определить их целостность было бы проблематично или даже невозможно из-за того, неясно где в ОЗУ лежат неизменяемые данные, а где изменяемые, линкер размещает их как хочет, а защищать каждый объект ОЗУ контрольной суммой выглядит как паранойя.

Поэтому проще всего — быть уверенным на 100%, что константные данные лежат в ROM. Как это сделать я хочу попробовать объяснить. Но для начала надо рассказать об организации памяти в ARM.

Организация памяти

Как известно, ядро ARM имеет гарвардскую архитектуру — шина данных и кода разделены. Обычно это означает, что предполагается наличие отдельной памяти для программ и отдельной памяти для данных. Но дело в том, что ARM является модифицированной гарвардской архитектурой, т.е. доступ к памяти осуществляется по одной шине, а уже устройство управления памятью обеспечивает разделение шин при помощи управляющих сигналов: чтения, записи или выбора области памяти.

Таким образом данные и код могут находится в одной и той же области памяти. В этом едином адресном пространстве может находится и ROM память и RAM и периферия. А это означает, что собственно и код и данные могут попасть хоть куда и это зависит от компилятора и линкера.

Поэтому чтобы различить области памяти для ROM(Flash) и ОЗУ их обычно указывают в настройках линкера, например в IAR 8.40.1 это выглядит вот так:

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

RAM в данном микроконтроллере находится с адреса 0x20000000...0х2001FFF, а ROM с 0x008000000...0x0807FFFF.
Вы легко можете поменять начальный адрес ROM_start на адрес ОЗУ, скажем RAM_start и конечный адрес ROM_end__ на адрес RAM_end__ и ваша программа будет полностью расположена в ОЗУ.
Вы даже можете сделать наоборот и указать ОЗУ в области памяти ROM, и ваша программа успешно соберется и прошьется, правда работать не будет :)
Некоторые микроконтроллеры, такие как, AVR изначально имеют раздельное адресное пространство для памяти программ, памяти данных и периферии и потому там такие фокусы не пройдут, а программа по умолчанию записывается в ROM память.

Все адресное пространство в CortexM единое, и код и данные могут размещаться где угодно. С помощью настроек линкера можно задать регион для адресов ROM и RAM памяти. IAR располагает сегмент кода .text в регионе ROM памяти

Объектный файл и сегменты

Выше я упомянул про сегмент кода, давайте разберемся, что это такое.

На каждый компилируемый модуль создается отдельный объектный файл, который содержит следующую информацию:

  • Сегменты кода и данных
  • Отладочную информацию в формате DWARF
  • Таблицу символов

Нас интересуют сегменты кода и данных. Сегмент это такой элемент, содержащий часть кода или данных, который должен быть помещен по физическому адресу в памяти. Сегмент может содержать несколько фрагментов, обычно один фрагмент на каждую переменную или функцию. Сегмент может быть помещен как в ROM так и RAM.
Каждый сегмент имеет имя и атрибут, который определяет его содержимое. Атрибут используется для определения сегмента в конфигурации для линкера. Например, атрибуты могут быть:

  • code — исполняемый код
  • readonly — константные переменные
  • readwrite — инициализируемые переменные
  • zeroinit — инициализируемые нулем переменные

Конечно есть и другие типы сегментов, например сегменты, содержащие отладочную информацию, но нас будут интересовать только те, которые содержат код или данные нашего приложения.

В общем и целом, сегмент это наименьший линкуемый блок. Однако при необходимости линкеру можно указать и еще более мелкие блоки (фрагменты). Этот вариант рассматривать не будем, обойдемся сегментами.

Во время компиляции данные и функции размещаются в различные сегменты. А во время линковки, линкер назначает реальные физические адреса различным сегментам. В компиляторе IAR есть предопределенные имена сегментов, некоторые из них я приведу ниже:

  • .bss — Содержит статические и глобальные переменные инициализируемые 0
  • .CSTACK — Содержит стек используемый программой
  • .data — Содержит статические и глобальные инициализируемые переменные
  • .data_init — Содержит начальные значения для данных в .data секции, если используется директива инициализации для линкера
  • HEAP — Содержит кучу, используемую для размещения динамических данных
  • .intvec — Содержит таблицу векторов прерываний
  • .rodata — Содержит константные данные
  • .text — Содержит код программы

Для того, чтобы понять где размещаются константы, нам будут интересны только сегменты
.rodata — сегмент в котором хранятся константы,
.data — сегмент в котором хранятся все проинициализированные статические и глобальные переменные,
.bss — сегмент в котором хранятся все проинициализированные нулем(0) статические и глобальные переменные .data,
.text — сегмент для хранения кода.

На практике это означает, что если вы определили переменную int val = 3, то сама переменная будет расположена компилятором в сегмент .data и помечена атрибутом readwrite, а число 3 может быть помещено либо в сегмент .text, либо в сегмент .rodata или, если применена специальная директива для линкера в .data_init и также помечается им как readonly.

Сегмент .rodata содержит константные данные и включает в себя константные переменные, строки, агрегатные литералы и так далее. И эта секция может быть размещена где угодно в памяти.

Вооружившись этими знаниями становится более понятно, что прописано в настройках линкера и почему:

place in ROM_region   { readonly }; // Разместить  сегменты .rodata и .data_init (константы и инициализаторы) в ROM: 
place in RAM_region   { readwrite, // Разместить сегменты .data, .bss, и .noinit 
                                     block STACK }; // и STACK  и HEAP в RAM

Т.е все данные помеченные атрибутом readonly должны быть помещены в ROM_region. Таким образом в ROM могут попадать данные из разных сегментов, но помеченные атрибутом readonly.

Отлично значит все константы должны быть в ROM, но почему же в нашем коде, в начале статьи константный объект все еще лежит в ОЗУ?

class WhantToBeInROM
{
private:
  int i;
public:
  WhantToBeInROM(int value): i(value) {}
  int Get() const
  {
    return i;
  }
};

const WhantToBeInROM myConstInROM(10);

int main()
{  
  std::cout << &myConstInROM << std::endl ;
}

Константные данные

Перед тем как прояснить ситуацию, вначале давайте вспомним, что глобальные переменные создаются в общей памяти, локальные переменные, т.е. перемененные, объявленные внутри «обычных» функций создаются на стеке или на регистрах, а статические локальные переменные также создаются в общей памяти.

Что это значит на языке С++. Давайте рассмотрим такой пример:

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

Это все константные данные. Но к любым из них применяется правило создания описанное выше, локальные переменные создаются на стеке. Поэтому, при наших настройках линкера, здесь должно быть так:

  • Case1 глобальная константа должна лежать в ROM. В сегменте .rodata
  • Case2 глобальная константа должна лежать в ROM. В сегменте .rodata
  • Case3 локальная константа должна лежать в RAM (константа создалась на стеке в сегменте STACK)
  • Case4 статическая константа должна лежать в ROM. В сегменте .rodata
  • Case5 локальная константа должна лежать в RAM (интересный случай, но он в точности идентичен Случаю 3.)
  • Case6 статическая константа должна лежать в ROM. В сегменте .rodata

Теперь посмотрим на отладочную информацию и сгенерированный map файл. Отладчик показывает по каким адресам лежат эти константы.

image

Как я уже говорил раньше, адреса 0x0800… это адреса ROM, а 0x200… это RAM. Давайте посмотрим, в какие сегменты распределил компилятор эти константы:

  .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

Четыре глобальных и статических константы попали в сегмент .rodata, а две локальные переменные не попали в map файл поскольку создаются на стеке и их адрес соответствует адресам стека. Сегмент CSTACK начинается по адресу 0x2000'2488 и заканчивается на 0x2000'0488. Как видно из картинки, константы как раз созданы на начале стека.

Глобальные и статические константы компилятор помещает в сегмент .rodata, расположение которого задается в настройках линкера.

Стоит отметить еще один важный момент, инициализация. Глобальные и статические переменные, в том числе и константы должны быть проинициализированы. И это может быть сделано несколькими способами. Если это константа лежащая в сегменте .rodata, инициализация происходит на этапе компиляции, т.е. значение сразу же прописывается по адресу, где находится константа. Если это обычная переменная, то инициализация может происходить путем копирования значения из ROM памяти по адресу глобальной переменной:

Например, если определена глобальная переменная int i = 3, то компилятор определил её в сегмент данных .data, линкер положил по адресу 0x20000000:
.data inited 0x2000'0000,
а её значении для инициализации (3), будет лежать в сегменте .rodata по адресу 0x8000190:
Initializer bytes const 0x800'0190
Если же написать такой код:

int i = 3;
const int c = i;

То очевидно, что глобальная константа с, инициализируется, только после того, как проинициализирована глобальная переменная i, т. е в runtime. В таком случае константа будет расположена в RAM

Теперь если мы вернемся к нашему

начальному примеру

class WhantToBeInROM
{
private:
  int i;
public:
  WhantToBeInROM(int value): i(value) {}
  int Get() const
  {
    return i;
  }
};

const WhantToBeInROM myConstInROM(10);

int main()
{  
  std::cout << &myConstInROM << std::endl ;
}

И зададимся вопросом: в какой же сегмент компилятор определил константный объект myConstInROM? И получим ответ: константа будет лежать в сегменте .bss, содержащий статические и глобальные переменные инициализируемые нулем(0).
.bss inited 0x2000'0004 0x4
myConstInROM 0x2000'0004 0x4

Почему? Потому что в С++ объект данных, который задекларирован как константа, и для которого необходима динамическая инициализация, располагается в read-write памяти и он будет проинициализирован во время создания..

В данном случае, происходит динамическая инициализация, const WhantToBeInROM myConstInROM(10), и компилятор положил этот объект в сегмент .bss, проинициализировав вначале все поля 0, а затем при создании константного объекта, вызвал конструктор для инициализации поля i значением 10.

Как же можно сделать так, чтобы компилятор расположил наш объект в сегменте .rodata? Ответ на этот вопрос прост, нужно всегда выполнять статическую инициализацию. Сделать это можно так:

1. В нашем примере видно, что в принципе компилятор может оптимизировать динамическую инициализацию в статическую, поскольку конструктор довольно прост. Для IAR компилятора можно пометить константу атрибутом __ro_placement
__ro_placement const WhantToBeInROM myConstInROM
При такой опции, компилятор расположит переменную по адресу в ROM:
myConstInROM 0x800'0144 0x4 Data
Очевидно, что такой подход не универсальный и в общем то очень специфический. Поэтому переходим к правильному способу :)

2. Он заключается в том, чтобы сделать конструктор constexpr. Мы сразу указываем компилятору использовать статическую инициализацию, т.е. на этапе компиляции, когда весь объект будет полностью «вычислен» заранее и все его поля будут известны. Все что нам нужно, это добавить constexpr к конструктору.

Объект улетает в ROM

class WhantToBeInROM
{
private:
  int i;
public:
  constexpr WhantToBeInROM(int value): i(value) {}
  int Get() const
  {
    return i;
  }
};

const WhantToBeInROM myConstInROM(10);

int main()
{  
  std::cout << &myConstInROM << std::endl ;
}

Итак, для того, чтобы быть уверенным, что ваш константный объект лежит в ROM, необходимо выполнять простые правила:

  1. Сегмент .text в котором размещен код должен находится в ROM. Настраивается в настройках линкера.
  2. Сегмент .rodata в котором размещены глобальные и статические константы должен быть в ROM. Настраивается в настройках линкера.
  3. Константа должна быть глобальной или статической
  4. Инициализация объекта должна быть статической, т.е.конструктор класса, объект которого будет константой, должен быть constexpr, либо вообще не определен(отсутствует динамическая инициализация)

Немного слов об constexpr конструкторе. Основное различие между переменными const и constexpr является то, что инициализация переменной const может быть отложена до времени выполнения. Переменная constexpr должна инициализироваться во время компиляции. Все переменные constexpr относятся к типу const.

Определение constexpr конструктора, должно удовлетворять следующим требованиям:

  • Класс не должен иметь виртуальных базовых классов.
    struct D2 : virtual BASE { 
      //error, D2 must not have virtual base class.
      constexpr D2() : BASE(), mem(55) { }    
    private:
      int mem; 
    };  
  • Каждый из типов параметров класса должен быть литеральным типом.
  • Тело конструктора должно быть = delete или = default. Или удовлетворять требованиям ниже:
  • В теле конструктора нет try catch блоков.
  • В теле конструктора может использоваться nullptr
  • В теле конструктора может использоваться static_assert
  • В теле конструктора может использоваться typedef, не определяющие классы или перечисления
  • В теле конструктора может использоваться директива и декларации using
  • Каждый нестатический член класса или базового класса должен быть проинициализирован
  • Конструкторы класса или базовых классовых, используемый для инициализации нестатических элементов членов класса и подобъектов базового класса, должны быть constexpr.
  • Инициализаторы для всех нестатических элементов данных, должны быть constexpr
  • При инициализации членов класса все преобразования типов должны быть допустимы в константном выражении. Например недопустимо использование reinterpret_cast и приведение из void* в указатель диугого типа

Неявно определенный конструктор по умолчанию является конструктором constexpr. Теперь давайте посмотрим на примеры:

Пример 1. Объект в 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;
}

Так лучше не писать, потому что, как только вы решите проинициализировать атрибут i, объект улетит в RAM

Пример 2. Объект в 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;
}

Пример 3. Объект в 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;
}

Пример 4. Объект в 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;
}

Пример 5. Объект в 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;
}

Пример 6. Объект в 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;
}

Пример 7. Ошибка компиляции

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

Пример 8. Объект в ROM, наследование абстрактного класса

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
    {
      return i + 1;
    }  
} ;

const Test test(10); //объект в ROM. i инициализируется на этапе компиляции constexpr конструктором, j также инициализируется constexpr конструктором  ITest

int main()
{
  std::cout << test.Give() << std::endl ;
  return 0;
}

Пример 9. Объект в ROM агрегирует объект, находящийся в 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
    {
      return i + 1;
    } 
    bool Set() const
    {
      obj.Set(100) ; //объект по ссылке можно изменять
      return true;
    }
} ;

const Test test(10, testImpl); //объект в ROM. Инициализируется на этапе компиляции constexpr конструктором

int main()
{
  std::cout << test.Set() << std::endl ;
  return 0;
}

Пример 10. Тоже самое но статические объект в 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
    {
      return i + 1;
    } 
    bool Set() const
    {
      obj.Set(100) ; //объект по ссылке можно изменять
      return true;
    }
} ;

int main()
{
  static TestImpl testImpl(1); //статический объект
  static const Test test(10, testImpl); //статический константный объект в ROM. Инициализируется на этапе компиляции constexpr конструктором
  std::cout << test.Set() << std::endl ;
  return 0;
}

Пример 11. А теперь константный объект нестатический и поэтому в 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
    {
      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;
}

Пример 12. Ошибка компиляции.

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

Пример 13. Ошибка компиляции

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

Пример 14. Объект в 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;
}

Ну и на последок, константный объект, содержащий массив, с инициализацией массива через constexpr функцию.

class Test
{
private: 
    int k[100];
  public: 
   constexpr Test(): k()
   {
     InitArray(); //вызываем constexpr функцию для инициализации массива
   }
    
   constexpr void InitArray()
   {
     int i = 0;
     for(auto it: k)
     {
       it = i++ ;       
     }
   }
   
   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;
}

Ссылки:
IAR C/C++ Development Guide
Constexpr constructors (C++11)
constexpr (C++)

Автор: Сергей

Источник

* - обязательные к заполнению поля