Об инициализации структур

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

Господь с вами. Хотел бы , чтобы он был int. А кто место экономит - пущай переопределяет или свой тип создает

Скажите спасибо, что не unsigned int. Язык это вполне допускает:

Дык так оно и есть везде на нормальных процессорах. Это только на микроконтроллерах такое. Я ж говорю - первый раз в жизни я столкнулся с тем, что bool - не int.

Вот так работает в продакшене, не на макете и не на столе… без всяких упаковок.

Спойлер
class SysState
{
public:
  int mode;
  int uptime;
  bool isDamp; //режим успокоения
  int errorCode;
  int progN;
  float tSet;
  int timeSet;
  int delaySet;
  float t;
  float tSystem;
  float tCorrection;
  bool waterOK;
  bool isASset;
  bool isAfterAS;
  int timeNow;
  int timeDStart;
  int timeCookStart;
  int timeCookEnd;
  int timeCookLeft;
  int timeLWstart;
  int timeLW;
  bool isLowT;
  bool startWith1;
  bool isWiFi;
  bool isTimer;
  bool isHeater;
  bool isPasterizationStarted;
  bool isBath;
  SysState() : mode(0),
               uptime(0),
               isDamp(true),
               errorCode(0),
               progN(1),
               tSet(25),
               timeSet(3600),
               delaySet(0),
               t(25),
               tSystem(25),
               tCorrection(0),
               waterOK(true),
               isASset(true),
               isAfterAS(false),
               timeNow(0),
               timeDStart(0),
               timeCookStart(0),
               timeCookEnd(0),
               timeCookLeft(0),
               timeLWstart(0),
               isLowT(false),
               startWith1(true),
               isWiFi(false),
               isTimer(false),
               isHeater(false),
               isPasterizationStarted(false),
               isBath(false)
               
  {
  }
  void loadState(void);
  void saveState(void);
};

void loadProg(void);
void saveProg(void);

extern SysState s;
//==================================
// В другом файле:
SysState s;
Preferences pref;

SemaphoreHandle_t xMutexState;

void SysState::loadState(void) {
  //xSemaphoreTake( xMutexState, portMAX_DELAY );
  pref.getBytes("sysState", (void *)(this), sizeof(SysState));
  //xSemaphoreGive( xMutexState);
}

void SysState::saveState(void) {
  xSemaphoreTake( xMutexState, portMAX_DELAY );
  pref.putBytes("sysState", (const void *)(this), sizeof(SysState));
  Serial.println("State saved");
  xSemaphoreGive( xMutexState);
}

Семафоры там не работают реально, это грязный кусок, но он именно из продакшена како-то-там версии.
Это ЕСП32 и никаких проблем не возникает. При изменении полей с структуре состояния системы, КОНЕЧНО нужно перезалить с форматированием и очисткой всего ЕЕПРОМ.

В общем, мужики, идея в том, чтобы писать не структуру целиком, а отдельные поля хороша по многим причинам. Но решение, которое предложил @Мишутк оставляет просто для всё тех же граблей. Что будет, если полем структуры является другая структура? Да, ничего, нормально запишется, только если в ней возникнут грабли с разным выравниванием, то мы прямо на них и наступим!

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

Решением здесь может служить такой приём: пишем программу записи / чтения так, чтобы она нормально записывала / читала тривиальные типы, но вызывала ошибку компиляции при попытке читать / писать что-то иное (например, структуру). Тогда, даже если программист и попытается наступить на грабли, компилятор сам стукнет его по лбу и заставит переписать потенциально опасный участок.

Как сделать, что с какими-то типами программа работала, а со всеми остальными вызывал ошибку? Один из способов – определить шаблон, который можно специализировать для всех допустимых типов, и сделать так, чтобы попытка использовать общий (неспециализированный) вариант вызывал ошибку компиляции.

Ну, и запись / чтение предпочтительнее делать методами самой структуры. Это очень ООП’но, когда тип сам знает как ему записываться, читаться и делать иные операции. Тогда, в случае чего, всё, связанное с типом меняется в одном месте (в нём самом) и не ищется по всей программе.

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

Файл 'eepromablestruct.h'
#ifndef 	eepromablestruct_h
#define	eepromablestruct_h

#include <avr/eeprom.h>

//
//	Шаблоны для чтения и записи в/из EEPROM различных базовых типов
//
//	Попытка чтения / записи типа, для которого явно не определена такая операция
//	приврдит к ошибке времени компиляции с диагностикой:
//		"Попытка использования неизвестного типа для записи/чтения EEPROM"
//
//	Все функции принимают адрес и значение и возвращают количество 
// записанных / прочитанных байтов.
//
//////////////////////////
//
//	ШАБЛОН ЗАПИСИ
//
//		Все поддерживаемые типы будут специализированы, а попытка использовать базовый шаблон
//		является признаком попытки записи неизвестного типа и должна приводить к ошибке компиляции
//
template<typename T> size_t eeprom_update(const size_t addr, const T f) { 
	((void)(addr));
	((void)(f));
	static_assert(sizeof(f)!=sizeof(f), "Попытка использования неизвестного типа для записи в EEPROM");
	return 0; 
}
//
//	Специализируем самые ходовые типы (всегда можно расширить перечень типов)
//
template<> size_t eeprom_update(const size_t addr, const float f) {
	eeprom_busy_wait();
	eeprom_update_float(reinterpret_cast<float *>(addr), f);
	return sizeof(float);
}
template<> size_t eeprom_update(const size_t addr, const uint8_t u8) {
	eeprom_busy_wait();
	eeprom_update_byte(reinterpret_cast<uint8_t *>(addr), u8);
	return sizeof(uint8_t);
}
template<> size_t eeprom_update(const size_t addr, const uint16_t w) {
	eeprom_busy_wait();
	eeprom_update_word(reinterpret_cast<uint16_t *>(addr), w);
	return sizeof(uint16_t);
}
template<> size_t eeprom_update(const size_t addr, const uint32_t dw) {
	eeprom_busy_wait();
	eeprom_update_dword(reinterpret_cast<uint32_t *>(addr), dw);
	return sizeof(uint32_t);
}
//
//	Производные типы
//
template<> size_t eeprom_update(const size_t addr, const bool b) {
	return eeprom_update(addr, static_cast<const uint8_t>(b));
}
template<> size_t eeprom_update(const size_t addr, const int16_t w) {
	return eeprom_update(addr, static_cast<const uint16_t>(w));
}
template<> size_t eeprom_update(const size_t addr, const int32_t dw) {
	return eeprom_update(addr, static_cast<const uint32_t>(dw));
}


//////////////////////////
//
//	ШАБЛОН ЧТЕНИЯ
//
//		Все поддерживаемые типы будут специализированы, а попытка использовать базовый шаблон
//		является признаком попытки чтения неизвестного типа и должна приводить к ошибке компиляции
//
template<typename T> size_t eeprom_read(const size_t addr, T & f) { 
	((void)(addr));
	((void)(f));
	static_assert(sizeof(f)!=sizeof(f), "Попытка использования неизвестного типа для чтения из EEPROM");
	return 0; 
}
//
//	Специализируем самые ходовые типы (всегда можно расширить перечень типов)
//
template<> size_t eeprom_read(const size_t addr, float & f) {
	eeprom_busy_wait();
	f = eeprom_read_float(reinterpret_cast<float *>(addr));
	return sizeof(float);
}
template<> size_t eeprom_read(const size_t addr, uint8_t & u8) {
	eeprom_busy_wait();
	u8 = eeprom_read_byte(reinterpret_cast<uint8_t *>(addr));
	return sizeof(uint8_t);
}
template<> size_t eeprom_read(const size_t addr, uint16_t & w) {
	eeprom_busy_wait();
	w = eeprom_read_word(reinterpret_cast<uint16_t *>(addr));
	return sizeof(uint16_t);
}
template<> size_t eeprom_read(const size_t addr, uint32_t & dw) {
	eeprom_busy_wait();
	dw = eeprom_read_dword(reinterpret_cast<uint32_t *>(addr));
	return sizeof(uint32_t);
}
//
//	Производные типы
//
template<> size_t eeprom_read(const size_t addr, bool & b) {
	eeprom_busy_wait();
	b = eeprom_read_byte(reinterpret_cast<uint8_t *>(addr));
	return sizeof(uint8_t);
}
template<> size_t eeprom_read(const size_t addr, int16_t & w) {
	eeprom_busy_wait();
	w = eeprom_read_word(reinterpret_cast<uint16_t *>(addr));
	return sizeof(uint16_t);
}
template<> size_t eeprom_read(const size_t addr, int32_t & dw) {
	eeprom_busy_wait();
	dw = eeprom_read_word(reinterpret_cast<uint16_t *>(addr));
	return sizeof(uint16_t);
}

#endif	//	eepromablestruct_h

Ну, а основной файл, который это вызывает вот такой:

Файл 'structsample.ino'
#include "eepromablestruct.h"

struct Point : public Printable {
	uint8_t id;
	uint32_t signature;
	float x;
	float y;

	Point(void) : id(0), signature(0), x(0), y(0) {}
	Point(const uint8_t _id, const uint32_t _signature, const float _x, const float _y) : id(_id), signature(_signature), x(_x), y(_y) {}

	size_t printTo(Print& p) const { 
		size_t res = p.print("Id: ");
		res += p.println(id);
		res += p.print("Signature: ");
		res += p.println(signature);
		res += p.print("X: ");
		res += p.println(x, 6);
		res += p.print("Y: ");
		res += p.println(y, 6);
		return res;
	}

size_t toEeprom(const size_t addr) const {
		size_t totalWritten = 0;
		totalWritten += eeprom_update(addr + totalWritten, signature);
		totalWritten += eeprom_update(addr + totalWritten, id);
		totalWritten += eeprom_update(addr + totalWritten, x);
		totalWritten += eeprom_update(addr + totalWritten, y);
		return totalWritten;
	}

size_t fromEeprom(const size_t addr) {
		size_t totalRead = 0;
		totalRead += eeprom_read(addr + totalRead, signature);
		totalRead += eeprom_read(addr + totalRead, id);
		totalRead += eeprom_read(addr + totalRead, x);
		totalRead += eeprom_read(addr + totalRead, y);
		return totalRead;
	}

};

void setup() {
	Serial.begin(9600);
	Point point0(3, 321, 3.141592, 2.718282);
	point0.toEeprom(100);

	Serial.println("Structure point0");
	Serial.println(point0);

	Point point1;
	Serial.println("Structure point1 (before reading)");
	Serial.println(point1);

	point1.fromEeprom(100);
	Serial.println("Structure point1 (after reading)");
	Serial.println(point1);
}
void loop(void) {}

и, да, я совершенно согласен с теми, кто говорит, что такие структуры (как у меня здесь) создавать не нужно. Надо идти от “больших” типов к маленьким, тогда и проблем меньше будет.

1 лайк

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

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