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

И если все настройки линкера правильные, студент рассчитывает, что вот в таком незамысловатом коде его константа лежит в 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 ;
}

Вы тоже можете пробовать ответить на вопрос: где лежит константа 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 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 ;
}



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


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

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

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

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


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

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

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

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

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

Объект улетает в 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 ;
}


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

Немного слов о 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 override
    {
      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 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;
}



Пример 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 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;
}



Пример 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 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;
}



Пример 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];
   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;
}


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

PS.
После очень полезной дискуссии с Valdaros необходимо добавить следующий момент касательный констант. Согласно стандарту С++ и вот этому документу N1076.pdf

1. Любое изменение константного объекта (за исключением mutable членов класса) в течении его жизни приводит к Undefined Behaviour. Т.е.

 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. Проблема заключается в том, что это работает только в период всей жизни константного объекта, а вот в конструкторе и деструкторе это не работает. Поэтому, вполне законно можно сделать так:

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

И это считается законным. Несмотря на то, что мы использовали constexpr конструктор, и constexpr функцию в нем. Объект идет прямиком в RAM.

Чтобы этого избежать, используйте вместо const — constexpr, тогда будет ошибка компиляции, которая укажет вам, что что-то не так, и объект не может быть константным.

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

Комментарии (26)


  1. arkamax
    03.06.2019 18:35

    Занудства ради: Whant… -> Want. А то глаз спотыкается.


    1. lamerok Автор
      03.06.2019 20:27

      Ага, поправил


    1. asm679
      04.06.2019 01:19
      +2

      Ctrl+Enter не пробовали? Чтобы не засорять комменты. ;)


      1. arkamax
        04.06.2019 01:42

        Век живи — век учись. Принято с благодарностью :)


  1. Valdaros
    03.06.2019 20:29

    Одна оговорка — стандарт не гарантирует размещения переменной в ROM. Есть гарантия что произойдет константная инициализация если статическая переменная (variable with static storage duration) инициализируется константным выражением (в C++20 можно проверить с помощью std::is_constant_evaluated()), но разницы в инициализации между static const Test test и static Test test нет с точки зрения стандарта. Усложняет ситуацию дефект языка CWG 2026, который был устранен лишь недавно, и баги в компиляторах (MSVC до сих пор не производит константную инициализацию constexpr конструктором).


    1. lamerok Автор
      04.06.2019 14:53

      Спасибо за ссылки и инфу…


    1. lamerok Автор
      04.06.2019 18:51

      Да, но дело в том, что возможность размещения в ROM статической переменной мною и не рассматривалась :)
      Я хотел показать только размещение в ROM статической константы. А она размещается туда только по настройкам линкера. Т.е. компилятор её определяет в сегмент констант c атрибутом readonly, а этот сегмент констант в настройках линкера, я могу задать в ROM памяти.
      И собственно важно, чтобы инициализация у такой константы была константная, что как вы и сказали, гарантируется стандартом, иначе она не попадет в сегмент констант с атрибутом readonly.
      Статическая же переменная будет в другом сегменте (.data, например), который скорее всего в настройках линкера будет лежать в RAM.


      1. Valdaros
        04.06.2019 20:02
        +1

        Немного занудства. В C++ констант нет, есть литералы, которые в C действительно называются константами. Спецификатор const это всего лишь контракт, за нарушение которого полагается undefined behavior. Компилятор, пользуясь этим неопределенным поведением, кладет эту переменную в ROM, но может и не положить, в том числе потому что есть легальные способы изменить const-qualified объект, например в деструкторе.


        1. lamerok Автор
          04.06.2019 20:34

          С первой частью согласен полностью, а как изменить const-qualified обьект легальным способом который изначально был обьявлен и является const? Разве это не будет автоматом UB?


          1. Valdaros
            04.06.2019 20:48
            +1

            Конструктором, деструктором, а так же любым способом его mutable поля.


            1. lamerok Автор
              04.06.2019 21:18

              Mutable это тоже контракт, насколько я понимаю, увидев mutable компилятор, конечно же не положит обьект в сегмент констант, по факту он уже физически не константа. А про конструктор и деструктор понял… Это получается изменение его модификатора до начала его времени жизни во время конструирования или уничтожения, когда правило UB и const сематики еще не работает. Но ведь constexpr конструктор такого не позволит…
              Еще раз спасибо, добавлю про mutable.


  1. pwl
    03.06.2019 21:10
    +2

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

    Только в том случае, если вам дела нет, до того где и как они лежат.

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

    И даже если такие константы у вас раскиданы по куче исходников, и лепить их в одну структуру неудобно, вы можете объявить свою секцию (в дополнение к .data .bss и т.п.), и уже потом рассказать линкеру куда вы хотите ее положить: хоть в RAM, хоть в ROM, хоть вообще абсолютный адрес указать.


    1. Alatarum
      04.06.2019 09:34

      +1
      Хотел написать ровно тот же коммент. Структуры и отдельные секции памяти — техники стары как мир. А размещение данных в ром я всегда рассматривал исключительно как инструмент сокращения потребления памяти. Обычно это относится к строкам и крупным статическим конфигурационным структурам, типа системного меню или дескрипторов dma. Так в одном проекте цепочка дескрипторов для обработки данных с ADC занимала почти десяток килобайт, дублировать всё это в Ram ваще не комильфо.


    1. lamerok Автор
      04.06.2019 13:38

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

      int p = 10;
      #define __safety _Pragma("location=\".myconstdata\"") 
      __safety const int con = p; //если сегмент .myconstdata в RAM, то все Ок, переменная будет лежать в сегменте myconstdata, а если в ROM, то вас обманули, con лежит в RAM
      

      И еще один момент, в случае, если инициализация не статическая, компилятор не положит вашу переменную в сегмент относящийся к ROM.
      И даже если вы сделалете, специальный сегмент для хранения критических данных в ОЗУ, то надо иметь ввиду, что эти данные все таки иногда, очень редко изменяются. Например, те же коэффициенты калибровки, пользователь сделал калибровку на поверке, и они впервые за 5 лет поменялись, в этот момент надо будет пересчитать всю контрольную сумму всего сегмента. В принципе, вариант рабочий, но есть и другие способы, не задействующие спец. фичи типа #pragma location и @. и опять же от того, что ваша переменная попадет не туда, он не всегда защищает! Поэтому, мое мнение, что нужно просто правильно писать код :) используя сам язык.


  1. Tsvetik
    04.06.2019 09:37

    Пытался я как-то писать "безопасный" код для МК.
    Следил за стеком, контролировал адреса входа и выхода из функций, проверял содержимое flash/eeprom контрольной суммой и мажорированием 2 из 3, сбрасывал вотчдог только при прохождении критических функций.
    В результате код превраитился в кучу вызовов некрасивых макросов и стал ужасно труден в отладке.


    Я пришел к выводу, что всякие надежные приемы, контроль целостности кода и данных должны поддерживаться на уровне компилятора или языка


    1. ser-mk
      04.06.2019 09:58

      А у вас есть примеры такого компилятора или языка, которые это поддерживают?


      1. Tsvetik
        04.06.2019 11:09

        Может, и нет. Не искал.
        GreenHills упирает на то, что их компилятор имеет какие-то сертификаты и делает рантайм проверки, но я подробно его не изучал.


  1. predator86
    04.06.2019 13:12

    Спасибо за статью. Очень познавательно.
    Так же хотелось узнать, есть ли способ положить в ROM строку перед которой установлена её длинна? Что то типа const char* my_str = {sizeof(«Text»), «Text»}; // my_str = {5,'T','e','x','t','\0'};


    1. Tsvetik
      04.06.2019 13:25

      const struct {
      int len;
      char* string;
      } some_struct = {sizeof(«Text»),«Text»};

      IAR кладет во flash


      1. lamerok Автор
        04.06.2019 13:48

        Ну там примеры почти про это :) Так как конструктор вы не объявили, он по умолчанию тут constexpr. Используя статическую агрегатную инициализацию. Получаете константу в ROM.

        struct TestStruct
        {
        int len;
        char string[];
        } ;
        
        const TestStruct some_struct = {5,"Text"};


        1. Tsvetik
          04.06.2019 13:51

          Я как-то на С++ почти не пишу. Не знал, что у структур тоже могут быть конструкторы.


          1. lamerok Автор
            04.06.2019 13:58

            в С++ структура это тоже класс, только все поля публичные


        1. predator86
          04.06.2019 14:12

          Спасибо! Это то что нужно.


      1. lamerok Автор
        04.06.2019 13:58

        только предупреждение будет :)


  1. acesn
    04.06.2019 13:12

    в последнем примере кода с инициализацей массива ошибка, в цикле надо использовать ссылку (auto& it)

    constexpr void InitArray()
    {
    	int i = 0;
    	for (auto& it : k)
    	{
    		it = i++;
    	}
    }
    


    1. lamerok Автор
      04.06.2019 13:19

      Точно, поправил!