Цитата
Ох уж этот загадочный C++.
Настоящие программисты наделают себе классов с шаблонами для моргания светодиодом, шевеления пинами, соединения строк и с гордостью нам эту лабуду демонстрируют.
От части согласен, от части нет. Если программировать ради программирования, тогда да. А если стоит конкретная задача, если работаешь постоянно с чем-то новым и видишь, что вроде-чтото подобное делал, но не совсем, тогда на помощь приходит C++ (а возможно и Rust).
Кроме того в C++, а тем более Rust компилятор постоянно проверяет программиста гораздо более строго, чем сишный(С практически ничего не проверяет).
Понимаю, что от части это холивары, но я не буду голословить а покажу конкретный например.
Класс-контейнер, хранящий один
исполняемый обьект любого размера и типа, но в рамка заданных критериев, то есть сигнатуру исполняемой функции.
Код
template<class T> class delegate;
template<class TRet, class... TArgs>
class delegate<TRet(TArgs...)>;
Очередь исполняемых обьектов:
Код
template<class T, int Size_w>class VdelegateFIFO_T;
Эта очередь принимает любой исполняемый обьект любого размера. Сама очередь фиксированного размера Size_w, определенного достаточным заранее.
Стоит задача - на шине PSI висит 10 устройств, нет, давайте хватит 5ти. Каждое из них совершенно разное. И использует шину совершенно непредсказуемо.
Это AЦП, DataFlash, Акселерометр, гироскоп и термометр.
АЦП выдает 1000 выборок в секунду. Гироскоп и аксель по 100. Термометр - одна выборка в секунду. Dataflаsh вообще редко используется, в основном когда юзер что-то в менюшке нажимает.
Как это все быстро и просто реализовать на C?
Как на C++ - да очень просто - используя нашу очередь(и производные от нее классы), однажды один раз написанную за несколько дней.
Для работы с SPI мы будем использовать DMA, но это не важно.
В драйвере SPI должна быть функция запуска DMA следующего вида:
Код
class Spi{
public:
void startDmaTransfer(const uint32_t* source, uint32_t* destination, int length, // с этим все ясно
const delegate<void()>& on_transferCompleted // а это наш любимый C++
);
};
Использовать это очень просто:
Код
class Accelerometer{
public:
void start_reading_sample(const delegate<void(uint16_t)>& callback){
// заплоняем буфер данными для запроса конкретной микросхемы
buffer[x] = y;
.....
// садимся на шину
set_chip_select(); // CS
// запускаем передачу DMA
spi.startDmaTransfer(buffer, buffer2, 8, [this, callback](){
// все, передача закончилась, слазим с шины убирая CS
clear_chip_select();
// сигнализируем того, кто нас вызвал о том, что данные прочитаны
callback(buffer[0] | (buffer[1]<<8));
});
}
....
Точно так же делается и для других устройств, но работа с каждым совершенно разная. На C это будет выглядеть, как куча структур, функций-обработчиков и void*, здесь же всю грязную работу сделает за нас компилятор, при чем проверит нас.
Это все классно, но что делать, если шина занята? Кто-то будет использовать какие-то мютексы, локи, busy флаги итд. Но мы сделаем все гораздо проще - на помощь приходит наша очередь исполняемых обьектов.
Нам понадобится очередь SPI-задач. создаем:
Код
class SpiTaskQueue : public VdelegateFIFO_T<void(SpiTaskQueue* queue), 48>{ ... }
SpiTaskQueue spi_task_queue(....);
Эта очередь также может быть приоритетная - сначала будут выполнятся более приоритетные задачи(например акселерометр, гироскоп и АЦП, а потом менее приоритетные, например термометр, а в конце уже dataflash.
Вот функция, которая считывает данные с акселерометра, в порядке его очереди и приоритета. У юзера вообще не болит голова что там с шиной да и вообще со всем остальным.
Код
bool Accelerometer::read_sample(const delegate<void(uint16_t)>& on_read_completed){
// добавляем задачу в очеред spi
bool r = spi_task_queue->enqueueTask([this, on_read_completed](){
// собственно сама задача. Этот код будет выполнен как только шина освободится, или сразу, если она уже свбодная
// запускаем чтение сэмпла
start_reading_sample([this, on_read_completed](uint16_t sample){
// А этот код будет выполнен, когда закончится чтение сэмпла из акселерометра
// сигнализируем очередь, что наша задача завершена и можно запускать любую другую
spi_task_queue->signal_taskCompleted();
// а теперь сигнализируем пользователя нашей функции, что данные прочитаны и передаем их ему
on_read_completed(sample);
});
}, ACCELEROMETER_TASK_PRIORITY); // очередь у нас приоритетная
if(!r){
printf("Accelerometer::read_sample error: SPI task queue full!\n");
}
return r;
}
Ну вот и все, задача решена. Все просто, понятно и довольно безопасно и без гонок. Точно так же реализовываем работу с остальными устройствами: АЦП,датафлеш,гироскоп и термометр.
Такое же можно и на C реализовать, но в C нет шаблонов, придется передавать кучу дополнительных параметров, например sizeof(AccelerometerReadSampleTask), нужно делать кучу структур(под каждый таск свою), кучу функций-колбеков и нигде не провтыкаться - компилятор в C делает гораздо меньше проверок, чем C++.
Мало того - сишная реализация
может оказаться менее быстрой, чем c++. В С вы будете описывать call-back-и явно, да еще и передавать дополнительные аргументы. В c++ большинство сделано на шаблонах, компилятор сам сгенерит оптимальный код и большинство функций будет встроено в недра самой очереди.
Я все больше присматриваюсь к языку Rust - он еще более строгий, чем c++, там даже все переменные по умолчанию const и много чего еще интересного.
В C++ у меня давно выработалась привычка писать не SomeClass* ptr, а const SomeClass* const ptr. Или не int x, a const int x.