Интерфейс без виртуализации

Здравствуйте!

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

Пытался сделать все через интерфейсный класс. Реализовал, но наличие в нем виртуальных методов существенно откушало память. Почти в 2 раза больше, чем было при использовании единого класса.

Решил полностью уйти от виртуализации. Пока сделал так.

Есть базовый класс TestA (с полем _a), через который идет обращение к объектам этого класса и объектам классов-потомков.
У него есть методы getA(), getB() и getC(). Также у него есть поле _id и метод getId().
От TestA наследуется TestAB (добавляется поле _b и реализация getB()).
Также есть класс TestC. Он не наследуется от TestA, имеет поле и реализацию getC().
Класс TestABC в свою очередь наследуется от TestAB и TestC, а класс TestAC - от TestA и TestC.

Каждому классу в конструкторе присваивается уникальный номер (_id). Этот номер используется классом TestA при выборе класса, в котором содержится нужная реализация getB() и getC().
Основные конструкторы класса разместил в секции protected, чтобы id не был виден снаружи.

Весь нижеследующий код выдуманный, нужен только для отработки решений.
enum CID { A, AB, AC, ABC };

struct TestA {
  constexpr TestA(const int a) : TestA(a, CID::A) {}
  int getA(void) const { return _a; }
  int getB(void) const;
  int getC(void) const;

  uint8_t id() const { return _id; }

protected:
  constexpr TestA(const int a, const int id) : _a(a), _id(id) {}
  const int _a;
  const uint8_t _id;
};

struct TestAB : public TestA {
  constexpr TestAB(const int a, const int b) : TestAB(a, b, CID::AB) {}
  int getB(void) const { return _b; }

protected:
  constexpr TestAB(const int a, const int b, const int id) : TestA(a, id), _b(b) {}
  const int _b;
};

struct TestC {
  constexpr TestC(const int c) : _c(c) {}
  int getC(void) const { return _c; }

protected:
  const int _c;
};

struct TestAC : public TestA, public TestC {
  constexpr TestAC(const int a, const int c) : TestAC(a, c, CID::AC) {}
  using TestC::getC;

protected:
  constexpr TestAC(const int a, const int c, const int id) : TestA(a, id), TestC(c) {}
};

struct TestABC : public TestAB, public TestC {
  constexpr TestABC(const int a, const int b, const int c) : TestABC(a, b, c, CID::ABC) {}
  using TestC::getC;

protected:
  constexpr TestABC(const int a, const int b, const int c, const int id)
    : TestAB(a, b, id), TestC(c) {}
};

int TestA::getB(void) const {
  switch (this->id()) {
    case CID::AB: return static_cast<const TestAB *>(this)->getB();
    case CID::ABC: return static_cast<const TestABC *>(this)->getB();
  }
  return -1;
}

int TestA::getC(void) const {
  switch (this->id()) {
    case CID::AC: return static_cast<const TestAC *>(this)->getC();
    case CID::ABC: return static_cast<const TestABC *>(this)->getC();
  }
  return -1;
}

void printABC(const TestA *p, const char *const name) {
  Serial.println(name);
  Serial.print("  getA() = ");
  Serial.println(p->getA());
  Serial.print("  getB() = ");
  Serial.println(p->getB());
  Serial.print("  getC() = ");
  Serial.println(p->getC());
  Serial.println();
}

void setup(void)  {
  Serial.begin(115200);

  TestA a(1);
  TestAB ab(2, 3);
  TestAC ac(4, 5);
  TestABC abc(6, 7, 8);

  printABC(&a, "TestA");
  printABC(&ab, "TestAB");
  printABC(&ac, "TestAC");
  printABC(&abc, "TestABC");
}

void loop(void) { }

Вопрос у меня в следующем.
Сейчас идентификатор класса хранится в каждом созданном объекте, что мне кажется не совсем правильным.
Напрашивается для каждого класса поле _id сделать статическим полем, как и метод getId().

Есть такая возможность? Если есть, то подскажите, пожалуйста, каким образом.

что это и зачем это здесь?

На ББ пишите - там памяти хоть ж…й жуй …

Не ясно - какого х… в testa включены методы getb, getc ???

Для того, чтобы к наследникам класса TestA можно было обращаться через класс TestA.

Да, забыл уточнить. целевой МК - Atmega328P. Среда Arduino IDE, стандартная.

А если появятся D, E, … Z - то будете опять править testa ??? Вы точно правильно поняли теорию ???

Да, придется править. Не придумал как сделать по-другому без виртуальных методов.

Память с какой целью экономите ? Не влезает в 32К или в прозапас ???

SRAM экономлю, а его всего 2К. Таблица виртуальных функций очень много отжирает. В реальном проекте уперся в потолок. Потому и захотелось сделать что-то похожее на интерфейсный класс, но без виртуализации.

Если не секрет, зачем такие сложности и в чём заключается проект?

Да проект-то несложный. Поддерживает температуру внутри камеры не выше заданной. В составе 3 датчика температуры, ЖКИ 16х2, энкодер, 4-х пиновый кулер, пищалка.
В “лоб” проект реализован. И работает.
Сложности начались, когда я захотел на его основе поразбираться с C++. И переделать е проект с учётом фишек этого языка. Это больше для саморазвития.

Я понимаю, что подход, выбранный мной для интерфейсного класса выглядит “костыльно”, но ничего другого придумать не смог.

Попробуйте чётко ответить на вопросы:

  1. сейчас у Вас все методы всех классов inline. Это допустимо? Или “в принципе нет, но так сделано для примера, чтобы файлы не плодить”?
  2. Вы говорите, что реализация методов отличается. В какой момент становится понятным какой именно метод должен применяться? Известно ли это на стадии компиляции (например, от константы зависит) или обязательно ковыряться во время исполнения (свичём разбирать кого вызывать), как у Вас в примере.

Если ответ на первый вопрос: “допустимо”, а на второй “можно определить на стадии компиляции”, то задача решается гораздо проще - можно при компиляции строить свой метод на каждый вызов – ни одной лишней операции в коде.

В реальном проекте у меня разделение. Часто вызываемые методы и большие по размеру у меня не inline. А те, что редко вызываются или состоят из пары строк - inline.
На самом деле объема PROGMEM хватит и для чисто inline методов. Так что можно сказать, что inline вполне допустим.

А вот на стадии компиляции к сожалению нет возможности определить какой объект будет выбран в данный момент. Имеется несколько объектов разных типов: TestA, TestAB, TestAC и TestABC. А обращение к ним осуществляется через указатель на TestA с передачей ему адреса одного из объектов, с которым и будем сейчас работать.

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

Вот смотрите пример:

void smethingWithSPI() {
// ... здесь делается что-то важное и нужное

// А вот здесь надо, если передача MSBFIRST, то сделать сдвиг 
//		payload >>= 1;
// а если LSBFIRST, то сделать сдвиг 
//		payload <<= 1;

// ... здесь делается что-то ещё более важное и нужное
}

Можно написать тупо в лоб:

void smethingWithSPI(const uint8_t direction) {
	uint8_t payload = 0;
// ... здесь делается что-то важное и нужное

	if (direction == MSBFIRST) payload >>= 1;
	else payload <<= 1;

// ... здесь делается что-то ещё более важное и нужное
}

Это будет примерно, как у Вас сейчас.

Но ведь не хочется, чтобы этот if постоянно исполнялся, тем более, что в программе обычно одно устройство и нужен реально только один вариант. Да и направление обычно известно уже при компиляции. Тут бы выручил if constexpr, но это только в самом свежем стандарте, мы до такого ещё не доросли.

Чтобы убрать if можно воспользоваться старым добрым препроцессором и написать что-то вроде:

#define DIRECTION MSBFIRST

// ...

void smethingWithSPI(void) {
	uint8_t payload = 0;
// ... здесь делается что-то важное и нужное

#if DIRECTION == MSBFIRST
	payload >>= 1;
#else
	payload <<= 1;
#endif

// ... здесь делается что-то ещё более важное и нужное
}

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

Современное решение – создать шаблонную функцию, специализированную на нужные случаи. Смотрите:

#define DIRECTION MSBFIRST

// ...
template <const int8_t direction> inline uint8_t shiftByte(const uint8_t bt) { return bt << 1; }
template <> inline uint8_t shiftByte<MSBFIRST>(const uint8_t bt) { return bt >> 1; }


void smethingWithSPI(void) {
	uint8_t payload = 0;
// ... здесь делается что-то важное и нужное

	payload = shiftByte<DIRECTION>(payload);

// ... здесь делается что-то ещё более важное и нужное
}

Это решит все проблемы. В код пойдёт нужный голимый сдвиг безо всяких if’ов. Такое решение можно использовать в библиотеках, оно не создаёт там проблем.

Так вот я думаю, что в Вашем случаю программу тоже можно собрать вот так из кирпичиков. Но для этого мне надо знать то, что Вам надо сделать, а не то, как Вы присобачили костыль. Даже тот Ваш код с виртуальными функциями мне бы больше помог понять что нужно, чем этот Ваш ответ.

Евгений Петрович, попробую показать на примере пары классов из реального проекта.
Начнем с класса (структуры) SParametr (сделал все inline).

В проекте реализация SParametr следующая:
#ifndef _PARAMETR_H_
#define _PARAMETR_H_

#include <Arduino.h>
#include <EEPROM.h>

struct SParametr : public Printable {
  // инициализируем на этапе компиляции и больше не меняем
  constexpr SParametr(const uint8_t &line_width, const uint8_t eeprom_addr, int16_t &value, const int16_t minimum, const int16_t maximum, const uint8_t precision,
                      const char *const name, const char *const unit, const char *const postfix, const bool *is_print_value = nullptr)
    : _line_width(line_width), _eeprom_addr(eeprom_addr), _value(value), _minimum(minimum), _maximum(maximum), _precision(precision), _name(name), _unit(unit),
      _postfix(postfix), _del(intPow(10, precision)), _is_print_value(is_print_value), _name_len(strlenUtf8(name)), _unit_len(strlenUtf8(unit)), _postfix_len(strlenUtf8(postfix)) {}

  // длина строки в UTF-8
  static constexpr size_t strlenUtf8(const char *const ptr) {
    return !*ptr ? 0 : ((((*ptr & 0xc0) != 0x80) ? 1 : 0) + strlenUtf8(ptr + 1));
  }
  // возвращает значение статического параметра LINE_WIDTH (максимальная длина строки), используется сторонними объектами
  static constexpr uint8_t lineWidth(void) {
    return LINE_WIDTH;
  }
  // возвращает указатель на себя, используется сторонними объектами
  SParametr *self(void) {
    return this;
  }
  // возвращает количество символов параметра
  size_t length(void) const {
    return _name_len + _unit_len + valueLength();
  }
  // возвращает количество символов имени параметра
  constexpr size_t nameLength(void) {
    return _name_len;
  }
  // возвращает количество символов единцы измерения параметра
  constexpr size_t unitLength(void) {
    return _unit_len;
  }
  // возвращает количество символов значения параметра
  size_t valueLength(void) const {
    size_t res = 0;
    uint16_t val = abs(_value);
    // считаем количество десятичных разрядов
    do {
      res++;
      val /= 10;
    } while (val);
    // добавляем дробные символы
    if (_precision) {
      if (res > 2) res++;  // просто добавляем символ "."
      else res += 2;       // добавляем символ "." и 1 разряд
    }
    // если число отрицательное, то еще знак '-'
    if (_value < 0) res++;
    return res;
  }
  // методы для работы с EEPROM
  void toEEPROM(void) {
    if (_eeprom_addr) {
      checkBounds();
      EEPROM.put(_eeprom_addr, _value);
    }
  }
  void fromEEPROM(void) {
    if (_eeprom_addr) {
      EEPROM.get(_eeprom_addr, _value);
      if (!checkBounds()) EEPROM.put(_eeprom_addr, _value);
    }
  }
  // Метод для изменения значения параметра
  void change(const int8_t value) {  // val может быть как положительным, так и отрицательным
    _value += value;
    // не проверял, но должно быть быстрее закомментированного варианта
    if (value == 10) {              // если value равно 10
      int16_t fract = _value % 10;  // а _value не кратно 10,
      if (fract) _value -= fract;   // то округляем (вниз) значение _value до десятков
    }
    // проверяем выход значения параметра за границы диапазона и корректируем при необходимости
    checkBounds();
  }

protected:
  // целочисленное возведение в степень
  static constexpr uint16_t intPow(const uint8_t value, const uint8_t exp) {
    return !exp ? 1 : value * intPow(value, exp - 1);
  }
  // потоковый вывод переменной
  size_t printTo(Print &p) const {
    // печатаем имя параметра
    p.print(_name);
    uint8_t res = _name_len;
    // печатем значение параметра
    if (_is_print_value == nullptr || *_is_print_value) {
      // выводим значение параметра
      if (_del) {
        int16_t val = _value / (int16_t)_del;
        uint16_t fract = abs(_value) % _del;
        res += p.print(val);
        res += p.print(".");
        size_t len = p.print(fract);
        // если число разрядов дробной части меньше количества знаков после точки, то забиваем остаток нулями
        while (len < (size_t)_precision) len += p.print("0");
        res += len;
      } else {
        res += p.print(_value);
      }
    } else {
      // забиваем место параметра пробелами
      size_t len = valueLength();
      res += len;
      while (len--) p.print(" ");
    }
    // печатем единицу измерения
    p.print(_unit);
    res += _unit_len;
    // печатаем постфикс, если есть
    if (_postfix_len) {  // если есть постфикс, то забиваем недостающие места (до конца строки) пробелами и печатам его
      res += _postfix_len;
      while (res < LINE_WIDTH) res += p.print(" ");  // LINE_WIDTH - длина строки у конкретного ЖКИ (1602)
      p.print(_postfix);
    }

    return res;
  }

protected:
  const uint8_t _eeprom_addr;   // адрес параметра в EEPROM
  int16_t &_value;              // указатель на значение переменной
  const int16_t _minimum;       // минимальное значение переменной
  const int16_t _maximum;       // максимальное значение переменной
  const uint8_t _precision;     // количество разрядов после запятой
  const char *const _name;      // короткое имя
  const char *const _unit;      // единица измерения
  const char *const _postfix;   // постфикс
  const uint8_t _del;           // делитель (1 байт, поддерживает максимум 2 знака после запятой), вычисляется на этапе компиляции
  const bool *_is_print_value;  // указатель на признак вывода значения переменной на экран
  const size_t _name_len;       // длина строки _name, вычисляется на этапе компиляции
  const size_t _unit_len;       // длина строки _unit, вычисляется на этапе компиляции
  const size_t _postfix_len;    // длина строки _postfix, вычисляется на этапе компиляции

  static const uint8_t LINE_WIDTH;  // максимальная длина строки (экрана) - статический параметр общий для всех экземпляров;
};

#endif  // _PARAMETR_H_

Описаны все возможные варианты внутри единой структуры SParametr.
Структура выводит на экран значение передаваемого по ссылке числа в формате с фиксированной запятой, выводит его на экран (с учетом наименования, единицы измерения, постфикса (если есть)). Число можно менять, читать (сохранять) его значение из (в) EEPROM. При изменении значения параметра он будет мигать.
Соответственно в структуре имеются все необходимые для работы с параметром поля.

Далее есть структура SScreen, предназначенная для отрисовки параметра на экране. К каждому экземпляру этой структуры привязан конкретный параметр.

Реализация SScreen (тоже все inline)
#ifndef _SCREEN_H_
#define _SCREEN_H_

#include "parametr.h"

// класс экрана, выводит на экран параметр (2 строка) с заголовком (1 строка)
// в данной реализации остаток строки до конца экрана забивается пробелами, если нужно другое то переопределяйте printTo()!!!
struct SScreen : public Printable {
public:
  // инициализируем на этапе компиляции и больше не меняем
  constexpr SScreen(SParametr &parametr, const char *const title)
    : _parametr(parametr), _title(title), _title_len(SParametr::strlenUtf8(title)) {}

	// изменяет значение привязанного параметра
  void changeValue(const int8_t value) {
    _parametr.change(value);
  }
	// возвращает указатель на привязанный параметр
  SParametr *getParametr(void) {
    return _parametr.self();
  }

protected:
  // потоковый вывод параметра
  virtual size_t printTo(Print &p) const {
    p.print('\f');						// управляющий символ FORM FEED (FF), устанавливает начало новой страницы, экрана
    // 1 строка
    p.print(_title);					// количество напечатанных символов берем не из print(),
    size_t res = _title_len;	// а расчитанное функцией strlenUtf8(), поскольку UTF-8 больше 1 байта
    // добиваем пробелами до конца строки
    while (res < _parametr.lineWidth()) res += p.print(" ");  // lineWidth() - длина строки у конкретного ЖКИ (1602)
    res += p.println();
    // 2 строка
    size_t len = p.print(_parametr);	// значение параметра выводится учетом кодировки UTF-8, количество символов правильное
    // добиваем пробелами до конца строки
    while (len < _parametr.lineWidth()) len += p.print(" ");  // lineWidth() - длина строки у конкретного ЖКИ (1602)
	  res += len;
		
    return res;
  }

protected:
  SParametr &_parametr;       // ссылка на параметр
  const char *const _title;   // заголовок (наименование параметра)
  const uint8_t _title_len;	// длина заголовка в UTF-8
};

#endif  // _SCREEN_H_

Ссылка на конкретный экземпляр SScreen передается экземпляру SMenuItem и выводится на экран ЖКИ в его методе show(). Реализацию SMenuItem не буду тут приводить. Она построена на основе Вашего урока про связанные списки с некоторыми дополнениями и изменениями.

В файле *.ino эти экземпяры SParametr и SScreen объявляются

следующим образом:
constexpr uint8_t LCD_COLUMNS_COUNT = 16;
constexpr uint8_t SParametr::LINE_WIDTH = LCD_COLUMNS_COUNT;

bool is_value_visible = true;  // признак мигания параметра

int16_t temp_current_top,  // текущая температура (верх)
  temp_current_middle,     // текущая температура (середина)
  temp_current_bottom,     // текущая температура (низ)
  fan_rmp,                 // обороты кулера
  pwm_duty,                // скважность ШИМ кулера текущая
  pwm_duty_min,            // скважность ШИМ кулера (минимальная)
  pwm_duty_max,            // скважность ШИМ кулера (максимальная
  backlight_lvl,           // уровень подсветки ЖКИ
  temp_set,                // заданная температура
  temp_critical,           // критическая температура
  reg_Kp,                  // настройка регулятора Кп
  reg_Ki,                  // настройка регулятора Ки
  reg_Kd;                  // настройка регулятора Кд

SParametr prm_temp_top(0, temp_current_top, 0, 0, 1, "T1=", "°C", ""),  
  prm_temp_middle(0, temp_current_middle, 0, 0, 1, "T2=", "°C", ""),  
  prm_temp_bottom(0, temp_current_bottom, 0, 0, 1, "T3=", "°C", ""),  
  prm_temp_critical(2, temp_critical, 40, 80, 0, "Tc=", "°C", "", &is_value_visible),
  prm_temp_set(4, temp_set, 20, 40, 0, "Ts=", "°C", "", &is_value_visible),
  prm_fan_rpm(0, fan_rmp, 0, 0, 0, "RPM=", "", ""),
  prm_pwm_duty(6, pwm_duty, 0, 255, 0, "PWM=", "", "", &is_value_visible),
  prm_backligth_lvl(8, backlight_lvl, 0, 255, 0, "Value=", "", "", &is_value_visible),
  prm_reg_Kp(10, reg_Kp, 0, 100, 1, "Kp=", "", "(prop)", &is_value_visible),
  prm_reg_Ki(12, reg_Ki, 0, 100, 1, "Ki=", "", "(int)", &is_value_visible),
  prm_reg_Kd(14, reg_Kd, 0, 100, 1, "Kd=", "", "(diff)", &is_value_visible),
  prm_pwm_min(16, pwm_duty_min, 0, 255, 0, "Minimum=", "", "", &is_value_visible),
  prm_pwm_max(18, pwm_duty_max, 0, 255, 0, "Maximum=", "", "", &is_value_visible);

SScreen scr_temp_top(prm_temp_top, "Current temp:"),
  scr_temp_middle(prm_temp_middle, "Current temp:"),
  scr_temp_bottom(prm_temp_bottom, "Current temp:"),
  scr_temp_critical(prm_temp_critical, "Critical temp:"),
  scr_temp_set(prm_temp_set, "Set temp:"),
  scr_fan_rpm(prm_fan_rpm, "Fan rate:"),
  scr_pwm_duty(prm_pwm_duty, "PWM duty:"),
  scr_backligth_lvl(prm_backligth_lvl, "Backligth level:"),
  scr_reg_Kp(prm_reg_Kp, "Regulator tune:"),
  scr_reg_Ki(prm_reg_Ki, "Regulator tune:"),
  scr_reg_Kd(prm_reg_Kd, "Regulator tune:"),
  scr_pwm_min(prm_pwm_min, "PWM duty tune:"),
  scr_pwm_max(prm_pwm_max, "PWM duty tune:");

Так вот. Параметры prm_temp_top, prm_temp_middle, prm_temp_bottom, prm_fan_rpm я вручную не меняю, их значения считываются с датчиков. Соответственно их не надо хранить в EEPROM, знать их максимум и минимум и т.д. А постфикс есть только у scr_reg_Kp, scr_reg_Ki и scr_reg_Kd.
Значит их поля, отвечающий за этот функционал, лишние и просто отжирают память.

Это сподвигло меня заняться оптимизацией кода, что и привело к созданию данной темы.

Код SParametr с костылями
#ifndef _PARAMETR_H_
#define _PARAMETR_H_

#include <Arduino.h>
#include <EEPROM.h>

enum PRM_ID { SIMPLE = 0,
              VARIABLE,
              POSTFIX };

// базовый класс, выводит на экран параметр со значением value в формате с фиксированной запятой. Тут же описаны все методы, которые есть у потомков, вызываются по ID класса из классов-потомков
struct SParametr : public Printable {
public:
  constexpr SParametr(int16_t &value, const uint8_t precision, const char *const name, const char *const unit)
    : SParametr(value, precision, name, unit, PRM_ID::SIMPLE) {}

  // длина строки в UTF-8
  static constexpr size_t strlenUtf8(const char *const ptr) {
    return !*ptr ? 0 : ((((*ptr & 0xc0) != 0x80) ? 1 : 0) + strlenUtf8(ptr + 1));
  }
  // возвращает значение статического параметра LINE_WIDTH (максимальная длина строки), используется сторонними объектами
  static constexpr uint8_t lineWidth(void) {
    return LINE_WIDTH;
  }
  // возвращает указатель на себя, используется сторонними объектами
  SParametr *self(void) {
    return this;
  }
  // возвращает количество символов параметра
  size_t length(void) const {
    return _name_len + _unit_len + valueLength();
  }
  // возвращает количество символов имени параметра
  constexpr size_t nameLength(void) {
    return _name_len;
  }
  // возвращает количество символов единцы измерения параметра
  constexpr size_t unitLength(void) {
    return _unit_len;
  }
  // возвращает количество символов значения параметра
  size_t valueLength(void) const {
    size_t res = 0;
    uint16_t val = abs(_value);
    // считаем количество десятичных разрядов
    do {
      res++;
      val /= 10;
    } while (val);
    // добавляем дробные символы
    if (_precision) {
      if (res > 2) res++;  // просто добавляем символ "."
      else res += 2;       // добавляем символ "." и 1 разряд
    }
    // если число отрицательное, то еще знак '-'
    if (_value < 0) res++;
    return res;
  }
  // Метод возвращает идентификатор класса
  uint8_t id(void) const {
    return _id;
  }
  // Метод для изменения значения параметра
  void change(const int8_t value);
  // Методы для работы с EEPROM
  void toEEPROM(void);
  void fromEEPROM(void);

protected:
  // Конструктор
  constexpr SParametr(int16_t &value, const uint8_t precision, const char *const name, const char *const unit, const uint8_t id)
    : _value(value), _precision(precision), _del(intPow(10, precision)), _name(name), _unit(unit), _name_len(strlenUtf8(name)), _unit_len(strlenUtf8(unit)), _id(id) {}
  // потоковый вывод переменной
  virtual size_t printTo(Print &p) const {
    // печатаем имя параметра
    p.print(_name);          // количество напечатанных символов берем не из print(),
    size_t res = _name_len;  // а расчитанное функцией strlenUtf8(), поскольку UTF-8 больше 1 байта
    // печатем значение параметра
    res += printParam(p);
    // печатем единицу измерения
    p.print(_unit);    // количество напечатанных символов берем не из print(),
    res += _unit_len;  // а расчитанное функцией strlenUtf8(), поскольку UTF-8 больше 1 байта

    res += printPostfix(p);  // печатаем постфикс, если есть (в наследниках)

    return res;
  }
  // вывод значения параметра
  size_t printValue(Print &p) const {
    size_t res;
    // выводим значение параметра
    if (_del) {  // если дробное число
      int16_t val = _value / (int16_t)_del;
      uint8_t fract = abs(_value) % _del;
      res = p.print(val);
      res += p.print(".");
      size_t len = p.print(fract);
      // если число разрядов дробной части меньше количества знаков после точки, то забиваем остаток нулями
      while (len < (size_t)_precision) len += p.print("0");
      res += len;
    } else {
      res = p.print(_value);
    }

    return res;
  }
  // вывод параметра, будет переопределяться в потомках
  size_t printParam(Print &p) const;
  // вывод постфикса, будет переопределяться в потомках
  size_t printPostfix(Print &p) const;
  // целочисленное возведение в степень
  static constexpr uint16_t intPow(const uint8_t value, const uint8_t exp) {
    return !exp ? 1 : value * intPow(value, exp - 1);
  }

protected:
  int16_t &_value;           // ссылка на значение переменной
  const uint8_t _precision;  // количество разрядов после запятой
  const uint8_t _del;        // делитель (1 байт, поддерживает максимум 2 знака после запятой), вычисляется на этапе компиляции
  const char *const _name;   // короткое имя
  const char *const _unit;   // единица измерения
  const size_t _name_len;    // длина строки _name, вычисляется на этапе компиляции
  const size_t _unit_len;    // длина строки _unit, вычисляется на этапе компиляции
  const uint8_t _id;         // идентификатор класса

  static const uint8_t LINE_WIDTH;  // максимальная длина строки (экрана) - статический параметр общий для всех экземпляров;
};

// потомок класса SParametr, можно изменять значение value в заданных пределах и сохранять в EEPROM
struct SVariableParametr : public SParametr {
public:
  // Конструктор
  constexpr SVariableParametr(const int8_t eeprom_addr, int16_t &value, const uint8_t precision, const int16_t minimum, const int16_t maximum,
                              const char *const name, const char *const unit, const bool *is_print_value = nullptr)
    : SVariableParametr(eeprom_addr, value, precision, minimum, maximum, name, unit, is_print_value, PRM_ID::VARIABLE) {}

  // методы для работы с EEPROM
  void toEEPROM(void) {
    if (_eeprom_addr) {
      checkBounds();
      EEPROM.put(_eeprom_addr, _value);
    }
  }
  void fromEEPROM(void) {
    if (_eeprom_addr) {
      EEPROM.get(_eeprom_addr, _value);
      if (!checkBounds()) EEPROM.put(_eeprom_addr, _value);
    }
  }
  // Метод для изменения значения параметра
  void change(const int8_t value) {  // val может быть как положительным, так и отрицательным
    _value += value;
    // не проверял, но должно быть быстрее закомментированного варианта
    if (value == 10) {              // если value равно 10
      int16_t fract = _value % 10;  // а _value не кратно 10,
      if (fract) _value -= fract;   // то округляем (вниз) значение _value до десятков
    }
    // проверяем выход значения параметра за границы диапазона и корректируем при необходимости
    checkBounds();
  }

protected:
  // Конструктор
  constexpr SVariableParametr(const int8_t eeprom_addr, int16_t &value, const uint8_t precision, const int16_t minimum, const int16_t maximum,
                              const char *const name, const char *const unit, const bool *is_print_value, const uint8_t id)
    : SParametr(value, precision, name, unit, id), _eeprom_addr(eeprom_addr), _minimum(minimum), _maximum(maximum), _is_print_value(is_print_value) {}
  // проверка выхода значения параметра за границы диапазона. Возвращает true, если в границах
  bool checkBounds(void) const {
    int16_t temp = _value;
    _value = _value < _minimum ? _minimum : (_value > _maximum ? _maximum : _value);

    return temp == _value;
  }
  // печать параметра
  size_t printParam(Print &p) const {
    size_t res;

    if (_is_print_value == nullptr || *_is_print_value) {  // если параметр мигания не привязан или его значение true
      // выводим значение параметра
      res = printValue(p);
    } else {
      // забиваем место параметра пробелами
      size_t len = valueLength();
      res = len;
      while (len--) p.print(" ");
    }

    return res;
  }

protected:
  const int8_t _eeprom_addr;    // адрес параметра в EEPROM
  const int16_t _minimum;       // минимальное значение переменной
  const int16_t _maximum;       // максимальное значение переменной
  const bool *_is_print_value;  // указатель на признак вывода значения переменной на экран при мигании параметра
};

// потомок класса SVariableParametr, добавляет в конце выводимого параметра постфикс, выравнивание по ширине строки (LINE_WIDTH)
struct SVariableParametrPostfix : public SVariableParametr {
public:
  // Конструктор
  constexpr SVariableParametrPostfix(const int8_t eeprom_addr, int16_t &value, const uint8_t precision, const int16_t minimum, const int16_t maximum,
                                     const char *const name, const char *const unit, const char *const postfix, const bool *is_print_value = nullptr)
    : SVariableParametrPostfix(eeprom_addr, value, precision, minimum, maximum, name, unit, postfix, is_print_value, PRM_ID::POSTFIX) {}

  // печать параметра
  size_t printPostfix(Print &p) const {
    size_t res = 0;

    if (_postfix_len) {  // если есть постфикс, то забиваем недостающие места (до конца строки) пробелами и печатам его
      res = _postfix_len;
      while (res < LINE_WIDTH) res += p.print(" ");  // LINE_WIDTH - длина строки у конкретного ЖКИ (1602)
      p.print(_postfix);
    }

    return res;
  }

protected:
  // Конструктор
  constexpr SVariableParametrPostfix(const int8_t eeprom_addr, int16_t &value, const uint8_t precision, const int16_t minimum, const int16_t maximum,
                                     const char *const name, const char *const unit, const char *const postfix, const bool *is_print_value, const uint8_t id)
    : SVariableParametr(eeprom_addr, value, precision, minimum, maximum, name, unit, is_print_value, id), _postfix(postfix), _postfix_len(strlenUtf8(postfix)) {}

protected:
  const char *const _postfix;  // постфикс
  const size_t _postfix_len;   // длина строеи _postfix, вычисляется на этапе компиляции
};


void SParametr::change(const int8_t value) {
  // если тип SVariableParametr (также включает SVariableParametrPostfix)
  switch (this->id()) {
    case PRM_ID::VARIABLE:
    case PRM_ID::POSTFIX:
      static_cast<SVariableParametr *>(this)->change(value);
  }
}

void SParametr::toEEPROM(void) {
  // если тип SVariableParametr (также включает SVariableParametrPostfix)
  switch (this->id()) {
    case PRM_ID::VARIABLE:
    case PRM_ID::POSTFIX:
      static_cast<SVariableParametr *>(this)->toEEPROM();
  }
}

void SParametr::fromEEPROM(void) {
  // если тип SVariableParametr (также включает SVariableParametrPostfix)
  switch (this->id()) {
    case PRM_ID::VARIABLE:
    case PRM_ID::POSTFIX:
      static_cast<SVariableParametr *>(this)->fromEEPROM();
  }
}

size_t SParametr::printParam(Print &p) const {
  switch (this->id()) {
    // если тип SVariableParametr (также включает SVariableParametrPostfix)
    case PRM_ID::VARIABLE:
    case PRM_ID::POSTFIX:
      return static_cast<const SVariableParametr *>(this)->printValue(p);
  }
  // если тип SParametr
  return this->printValue(p);
}

size_t SParametr::printPostfix(Print &p) const {
  // если тип SVariableParametrPostfix
  if (this->id() == PRM_ID::POSTFIX)
    static_cast<const SVariableParametrPostfix *>(this)->printPostfix(p);
  // иначе ничего не выводим
  return 0;
}

#endif  // _PARAMETR_H_

Тогда объявление экземпляров SParametr и его наследников в файле *.ino

будет выглядеть так:
SParametr prm_temp_top(temp_current_top, 1, "T1=", "°C"),  
  prm_temp_middle(temp_current_middle, 1, "T2=", "°C"),  
  prm_temp_bottom(temp_current_bottom, 1, "T3=", "°C"),  
  prm_fan_rpm(fan_rmp, 0, "RPM=", "");

SVariableParametr prm_temp_critical(2, temp_critical, 40, 80, 0, "Tc=", "°C", &is_value_visible),
  prm_temp_set(4, temp_set, 20, 40, 0, "Ts=", "°C", &is_value_visible),
  prm_pwm_duty(6, pwm_duty, 0, 255, 0, "PWM=", "", &is_value_visible),
  prm_backligth_lvl(8, backlight_lvl, 0, 255, 0, "Value=", "", &is_value_visible),
  prm_pwm_min(16, pwm_duty_min, 0, 255, 0, "Minimum=", "", &is_value_visible),
  prm_pwm_max(18, pwm_duty_max, 0, 255, 0, "Maximum=", "", &is_value_visible);

SVariableParametrPostfix prm_reg_Kp(10, reg_Kp, 0, 100, 1, "Kp=", "", "(prop)", &is_value_visible),
  prm_reg_Ki(12, reg_Ki, 0, 100, 1, "Ki=", "", "(int)", &is_value_visible),
  prm_reg_Kd(14, reg_Kd, 0, 100, 1, "Kd=", "", "(diff)", &is_value_visible);

Все остальное остается без изменений.
Вариант с виртуальными методами не буду приводить. В нем виртуальными были объвлены все методы, которые не нужны в базовом классе SParametr, но есть у его потомков.

Теперь что ксается шаблонов. Вы уже показывали реализацию на шаблонах функции подсчета символов в формате UTF-8. Но я, к своему стыду, так и не удосужился в этих шаблонах разобраться. Буду наверстывать упущенное. А до той поры мне не понятен Ваш пример из предыдущего сообщения.

А зачем им быть виртуальными? Виртуальным, обычно, делается метод, который нужен (и реально вызывается) в базовом классе, но там пока неизвестно как его реализовывать. А если он в базовом классе не вызывается, зачем ему там вообще быть?

Именно так, не правильно выразился. А виноват коньяк, будь он проклят! (с)

Выкиньте свои классы, методы, вируальные ненужности. Задача простая, для нее нужно простое решение.

Все эти сложности с классами нужны лишь авторам книг по С++, чтобы было чем зарабатывать :))). Ну и плюс к тому, “хорошо”, добротно написанный С++ код, с наследованием, виртуалоьными методами, невозможно ни читать ни отлаживать.

PS: я даже не говорю про то, что RTTI потащится вслед за этими изысками, и сожрет вам всю память.

1 лайк

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

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

1 лайк

Embedded программист показывает языку C++ его место.

PS. Ничего личного, просто юмор. Сам по возможности избегаю этого “си плюс минус”.