ООП проектирование - обращение к дочернему классу через родительский

В связи с соседней темой про запуск библиотеки SD.h на аппаратном или софтовом SPI хотел спросить опытных…

Чтобы готовая библиотека могла работать как с аппаратным, так и софтовым СПИ, создадим два класса Hard & Soft SPI . Интерфейс у них, понятно, будет одинаковый - совместимый со стандартным SPI Arduino:

Классы
class HardSPI {
    public:
	// var    
    uint32_t  _clock = 4000000;
    uint16_t  _bitOrder = LSBFIRST;
    uint8_t   _dataMode = SPI_MODE0;
	
	// methods		
    HardSPI(); 
    void SPI_Settings(uint32_t clock, uint16_t bitOrder, uint8_t dataMode);
    void beginTransaction(SPISettings settings);
    void endTransaction();		
    uint8_t transfer(uint8_t);		
};

class SoftSPI {
    public:
	// var    
    uint32_t  _clock = 4000000;
    uint16_t  _bitOrder = LSBFIRST;
    uint8_t   _dataMode = SPI_MODE0;
	
	// methods		
    SoftSPI(uint8_t mosi, uint8_t miso, uint8_t sck); 
    void SPI_Settings(uint32_t clock, uint16_t bitOrder, uint8_t dataMode);
    void beginTransaction(SPISettings settings);
    void endTransaction();		
    uint8_t transfer(uint8_t);		
};

Чтобы обращаться к ним единообразно, создадим для них виртуальный базовый класс и унаследуем реальные классы от базового:

BaseSPI
class BaseSPI {
    public:
	// var    
    uint32_t  _clock = 4000000;
    uint16_t  _bitOrder = LSBFIRST;
    uint8_t   _dataMode = SPI_MODE0;
	
	// methods		
    BaseSPI() {}; 
    virtual void SPI_Settings(uint32_t clock, uint16_t bitOrder, uint8_t dataMode) =0;
     virtual void beginTransaction(SPISettings settings) =0;
    virtual  void endTransaction() =0;	
    virtual uint8_t transfer(uint8_t) =0;	
};

class HardSPI : public BaseSPI {
    public:
	// var    
    // defined in base class
	// methods		
    HardSPI(); 
    void SPI_Settings(uint32_t clock, uint16_t bitOrder, uint8_t dataMode);
    void beginTransaction(SPISettings settings);
    void endTransaction();		
    uint8_t transfer(uint8_t);		
};

class SoftSPI  : public BaseSPI {
    public:
	// var    
     // defined in base class
	// methods		
    SoftSPI(uint8_t mosi, uint8_t miso, uint8_t sck); 
    void SPI_Settings(uint32_t clock, uint16_t bitOrder, uint8_t dataMode);
    void beginTransaction(SPISettings settings);
    void endTransaction();		
    uint8_t transfer(uint8_t);		
};

Теперь мы можем, в зависимости от потребности, работать через общий интерфейс с обоими SPI классами:

BaseSPI *SPI = new SoftSPI(mosi, miso, sck); 

Собственно, наконец я подхожу к вопросу, ради которого все это написал.
В строчке выше мы создаем идентификатор SPI как ссылку на базовый класс. Однако большинство из библиотек Ардуино обращаются к SPI непосредственно, а не по ссылке.
Например в той же либе SD.h обращения идут как

SPI.endTransaction();	

Но к нашему базовому классу надо обращаться как

SPI->endTransaction();	

Можно, конечно, определить подобный дефайн

#define mySPI (*SPI)

но это же костыль…

Читая книжки, я понял что есть вариант создать класс-обертку, котоорый будет содержать ссылку на наш SPI класс и вызывать его методы по ссылке:

class HandlerSPI {
    public:
	// var    
    BaseSPI *pSPI;
	// methods		
    HandlerSPI(BaseSPI *ptr): pSPI(ptr); 
    void SPI_Settings(uint32_t clock, uint16_t bitOrder, uint8_t dataMode);
    void beginTransaction(SPISettings settings);
    void endTransaction();		
    uint8_t transfer(uint8_t);		
};

но получается тогда в нем нам придется продублировать все методы базового класса

void HandlerSPI ::beginTransaction(SPISettings settings)
   {pSPI -> :beginTransaction(settings);}

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

А для чего его в куче создавать ???

Ничего не понял.

  1. Кому и для чего понадобился указатель на базовый класс?
  2. Если уж так надо, почему нельзя использовать ссылку?

Давайте, как мы с новорегами - нужен пример кода.

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

У меня вот такое скомпилировалось, нинаю, будет ли работать. Обращение к методам MySPI - через точку.


class TBaseClass {
public:
    TBaseClass() {}

    virtual void sendBytes(uint8_t* bytes) = 0;
};

class TChild1Class : public TBaseClass {
public:
    TChild1Class() :TBaseClass() {}

    void sendBytes(uint8_t* bytes) override {};
};

class TChild2Class : public TBaseClass {
private:
    uint8_t FPinCS;
    uint8_t FPinClock;

    TChild2Class() = delete;
public:
    TChild2Class(uint8_t APinCS, uint8_t APinClock) : TBaseClass() {
        FPinCS = APinCS;
        FPinClock = APinClock;
    }

    void sendBytes(uint8_t* bytes) override {};

};

TBaseClass& MySPI = *(new TChild2Class(2, 3));

вызов MySPI.sendBytes(nullptr); не вызывает идиосинкразии у компилятора

1 лайк

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

Я же Вам задал вопрос:

Привести-то можно. Только зачем? Объясните. Иначе непонятна проблема.

Очевидно, чтобы единообразно работать и с Soft и с Hard SPI. И не через указатели, а по ссылке, чтоб не писать кажный раз ‘->’

1 лайк

Евгений, прошу прощения, просто не успел Вам ответить. Предыдущий ответ был Командиру.

Ваш вопрос мне непонятен - Вы спрашиваете зачем вообще нужен базовый класс или почему я использую именно указатель?
На первый вариант ответил ДедСимен ниже - потому что мне нужен единообразный интерфейс для разных дочерних классов. Если же вопрос “Зачем указатель?” - по глупости. Столько лет “типа программирую” на Си, но концепция “ссылка это не укзатель” у меня в голове так и не отложилась…

Спасибо. На первый взгляд то что нужно. Попробую в деле.

Ну, вопрос-то был однозначен

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

Т.е. я реально не понимаю для чего это надо.

Ну, описали Вы два дочерних класса, ну и объявляйте их просто статически. Для чего “нидирекция-то”? Или Вы хотите одновременно в одном и том же коде использовать и то, и другое под одним и тем же именем? Но тогда ссылка не прокатит - её нельзя ничего присвоить после инициализации.

Объясните толком, что нужно, а лучше дайте пример проблемного кода.

у меня нет кода, я на этапе проектирования.

Попытаюсь обьяснить, что нужно.
Имеется девайс, к которому уже написаны два класса - для Хард и Софт SPI. Хард работает на аппаратных пинах, Софт - на любых.
Хочу создать такой класс-обертку для двух классов, который принимал бы на входе номера пинов, проверял их на соответствие аппаратным СПИ, если да - выхывал бы конструктор ХАРД класса, если нет - Софт.
И чтобы результатом работы был идентификатор, который можно подставить в любую использующую SPI библиотеку без переписывания ее кода.

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

да, тогда вариант Деда не подходит. Мне нужна возможность изменять тип SPI по ходу программы. Класс для хард СПИ создается по дефолту. Но нужна возможность на старте программы задать другие пины и переопределить системный класс из хард в софт, сохранив его имя. Или создать новый класс под тем же именем.
Тогда только вариант через 4 класса

( отредактировано)

Похоже, Вы себя накрутили.

Если не нужно использовать ДВА девайса через разные классы одновременно, то не вижу проблемы.

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

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

А хочется сделать так, чтобы с хардовым СПИ библиотеки работали совсем без правки, а если юзер продвинутый и желает сменить дефольтные пины - тогда уж пусть заморачивается и добавляет вызов нового класса в код.

Про пины то всё равно надо как то сообщить = переделать пример …

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

Я же говорю, Вы себя накрутили! Выдохните!

Кто мешает Вам в Вашем классе SPI создавать то, что Вы называете “дефолтным классом”? Ну, вот, кто мешает? Только сразу тот, который нужен, без дёрганий.

Обязательно будут и те кто захочет на дефолтных пинах гонять SOFT SPI - им как быть ?

не понимаю, сорри
Вы предлагаете использовать классы Хард и Софт по отдельности, без создания выше них базового класса, верно?
Если я заранее создаю дефолтный SPI как экземпляр HardSPI, как потом в коде, при необходимости, создать под этим же именем экземпляр SoftSPI ?

эти пусть идут в ж:) и пишут сами