Comments 59
Один вопрос: virtual-методы заставляют компилятор всунуть в класс указателя на vtable, в конструктор — инициализацию указателя и еще куда-нибудь — саму vtable.
Вы не думали над тем, чтобы реализвовать полиморфизм времени компиляции с помощью CRTP? Результат будет тот же, размер объекта меньше на 1 указатель.
Когда начинаешь считать байты, вся красота сходит и получается голый Си и портянка кода в 50 страниц одним куском.
Например, состояния кнопки, можно впихнуть по две кнопки в байт, а верхнего уровня класс по 8 кнопок на байт (по биту) может держать.
Вся эта объектность явно не экономит память, но позволяет быстро слепить и получить лаконичный понятный код.
В примере с кнопкой, я бы предположил, что компилятор сделает ранее связывание и не будет никаких лишних указателей. Можно проверить сделав sizeof у объекта.
Проверил — размер PressButton ровно на 1 байт больше (byte sw), чем у родительского SmartButton.
Беда начинается, если бы были указатели на объекты.
byte btPin; // 1
state btState = state::Idle; // 2
input btInput = input::Release; // 2
unsigned long pressTimeStamp; // 4
//---
// Total: 9
vtable_t* vtable // 2
// ---
// Grand total: 11 :)
Пруф того, что enum и указатель — 2 байта в коде выглядит так: create.arduino.cc/editor/4eyes/dcbd34bd-ff67-428a-b67e-116273eae6f2/preview
Мне не нравится, например, что надо конструкторы у нового класса надо писать. Лучше б сгенерить.
CRTP не даст ли доступ к приватным переменным порождённому классу? Это было бы плохо и создаёт потенциальные проблемы.
CRTP не даст ли доступ к приватным переменным порождённому классу?Нет, конечно. private член доступен только внутри класса. Он может быть переопределен в наследнике, хоть с шаблонами, хоть без (это один из красивых сюрпризов С++), во вызов или доступ к нему возможны только изнутри класса.
Мне не нравится, например, что надо конструкторы у нового класса надо писать. Лучше б сгенерить.Я не совсем понял, что имеется в виду под «писать».
Если «реализовать» — то это не обязательно, до тех пор пока вас устраивает дефолтная реализация. Дефолтная реализация конструктора по умолчанию не делает ничего, а копирования — вызывает конструктор копирования родителя, а потом конструкторы копирования всех членов класса. Для POD типов (int, float, ..., и struct без конструкторов) — читай копирует побайтно.
Я не совсем понял, что имеется в виду под «писать».
Вот это:
PressButton(byte bt_pin) : SmartButton(bt_pin) {};
Что бы подтянуть в наследник конструкторы базового класса можно написать так:
using SmartButton;
это один из красивых сюрпризов С++
Надо покурить это на досуге, спасибо.
Подробнее хорошо описано тут: isocpp.org/wiki/faq/strange-inheritance#private-virtuals
void Button::draw() // public virtual
{
// Please do not forget to call this method from derived class!
// I promise I'll one kitten each time you forget it
eraseBackground();
drawBorder();
drawText();
}
void ImageButton::draw() // public virtual
{
Button::draw(); // did not forget
drawImage();
}
Можно сделать так:
void Button::draw() // public NON-virtual
{
eraseBackground();
drawBorder();
drawText();
doCustomDraw();
}
virtual void Button::doCustomDraw() {} // private virtual
virtual void ImageButton::doCustomDraw() // private virtual
{
drawImage();
}
void Window::draw()
{
Button* someButton = getSomeButton();
someButton->draw(); // always erase backrgound, draw text & border. Probably draw image
}
Если можно обойтись ранним, лучше им.
причём 2 забирает слово virtual :( причём один раз и путь лучше в базовом классе.
#ifndef MYLIB_H
#define MYLIB_H
#if ARDUINO >= 100
#include <Arduino.h>
#else
#include <WProgram.h>
#endif
// Ваш код здесь
#endif
Почему у #define MYLIB_H есть отступ, а у всего остального нет?
К слову, раз уж взяли С++11, используйте enum class, а не просто enum. Это сильно снижает количество глупых ошибок.
Здесь чем проще, тем лучше. Для начинающих.
Отступ, хмм… не так принципиально ведь?
Отступ, хмм… не так принципиально ведь?
Разумеется, не принципиально, просто странно.
Главное — не доводить до такого
(шепотом дилера) Между прочим, а вы слышали про #pragma once?
Я использовал
#ifndef A
#define A
//
#endif
ещё в 1985 году, когда только Цэ у нас появился.
Привычка.
Остаются, конечно, некоторые сложные случаи, когда она не работает, но они редки.
Но дело ваше :)
#if ARDUINO >= 100
#include <Arduino.h>
#else
#include <WProgram.h>
#endif
Цитата из robocraft.ru/blog/arduino/751.html
В Arduino IDE версии 1.0, разработчики переименовали файл WProgram.h в Arduino.h, поэтому, чтобы старые библиотеки заработали в новой IDE — нужно просто открыть файлы библиотеки (.h и .cpp) и если в них встречается строчка
Антипаттерн на антипаттерне. Почему ваша Toggle кнопка, знает то что она делает (переключает лед).
Класс кнопки должен описывать только изменение её состояния. А то захочется вам сделать тугл но не для леда и будете городить новый класс
У вас нарушается и прицип инверсии зависимостей, и принцип единственной ответственности
Я не программист. Про бульдозер я загнул, конечно, но я закончил с программированием промышленным лет 30 назад. Я ничего не понял про евент-обсервер и нижеследующие принципы.
Нет ли желания показать на примере с тоглом и евент-обсервером?
Наверно имеется ввиду примерно такая конструкция:
class IButtonClickObserver {
public:
virtual void onButtonToggle () = 0;
};
class PushButton {
public:
PushButton (IButtonClickObserver *observer) : observer_ (observer){}
private:
void onToggle () {observer_->onButtonToggle();}
IButtonClickObserver *observer_;
};
class MyApp : public IButtonClickObserver {
MyApp () : button_ (this){}
void onButtonToggle () override final { /* do smthing useful */}
private:
PushButton button_;
};
Это с использованием виртуальных функций. Можно с шаблонами, код будет посложнее, однако удастся избежать лишнего указателя на vtable и убрать интерфейс observer. Можно так и так, но первый вариант на мой вкус удобочтимее.
#include <functional>
class PushButton {
public:
template <typename K, void (K::*func)()>
void setObserver(K *object) {
func_ = func_wrapper<K, func>;
object_ = object;
}
protected:
void onToggle () {func_ (object_);}
template <class K, void (K::*func)()>
static void func_wrapper(void *obj) {
return (static_cast<K *>(obj)->*func)();
}
std::function<void(void *obj)> func_ = nullptr;
void *object_;
};
class MyApp {
public:
MyApp () {
button_.setObserver<MyApp, &MyApp::onToggle> (this);
}
void onToggle ();
protected:
PushButton button_;
};
Это для меня, увы, слишком сложно. Я в такие дебри ещё не лазал.
На буднях кодеров попрошу пояснить, что это такое. :)
godbolt.org/g/yy3TWp
К сожалению, я не силен в C++.
1) Ивент-обсервер:
У вас есть события и обработчики. Событие к примеру "нажатие кнопки", "правый клик мышкой" и тд и тп
Обработчик это объект который "слушает"(ждет) событие и когда оно происходит выполняет какие-то действия
В этом случае ваша кнопка не знает что будет происходить по её нажатию. Условно можете это представлять как реальную кнопку, которая не знает, что будет происходить в цепи когда на неё нажмут, она лишь замыкает или размыкает контакт.
По аналогии вы создали кнопку, которая может зажигать только светодиод. В реальном мире это выглядело бы как кнопка связанная с светодиодом, которая не может быть использованна ни с чем кроме светодиода. Не логично, лучше создать универсальную кнопку)
Чем больше ваши классы связанны между собой тем сложнее будет расширять (масштабировать) систему в будущем.
Это привязывает вас в будущем к определеннию кнопки заново каждый раз с постоянным повторением части логики.
В оптимальном случае вы будете довлять только "что-то новое", то есть обработчик нового события
Здесь компромисс пока что. В атмеге нет динамической памяти, STL и тп. Памяти вообще мало — 2Кб на данные.
Идею я понял с обсервером, не спешу, но подумаю.
А если по-старорежимному сделать класс toggle с колбеком? Хотя нет, каждый колбек 2 байта жрёт данных, это дорого для кнопочек.
Покурю про обсерверы ещё.
А по поводу SOLID.
Это 5 принципов, следуя которым, вы в 90% случаев напишите хороший код, который вы сможете легко тестировать, масштабировать и поддерживать в будущем
Arduino IDE позволяет использовать синтаксис C++11, оказывается. То есть, там очень развитый объектно-ориентированный язык.
Совсем не так. Там полноценный С++, но из среды выполнения выпилили исключения и нет всяких STL, ибо на эмбеддед мало памяти и вообще динамическая аллокация — зло.
Надо будет набросать статейку про тюнинг ардуиновского тулчейна…
habrahabr.ru/post/346202
Оффтоп про Arduino-библиотеки.
Пытался класть библиотеку рядом со скетчем и инклюдить через относительный пути
#include "lib/SomeLib/SomeLib.h"
и получал ошибки линкера на функции и пр. из библиотеки.
Можно как-нибудь библиотеки локально хранить, чтобы потом тем, кто этот код будет использовать не требовалось их вручную ставить?
Я же написал в статье, куда положить и как, чтобы IDE увидел их как библиотеки. Не надо писать пути в include. Надо выполнить эти условия, а не изобретать велосипед.
Чтобы кто-то мог использовать этот код — его лучше держать на гитхабе. Да, включая сами скетчи тоже.
Чтобы кто-то мог использовать этот код — его лучше держать на гитхабе. Да, включая сами скетчи тоже.
Да я об этом и веду речь.
Есть некая библиотека AAA. Я ее скачал с гитхаба или через встроенный менеджер скачал — так или иначе она будет доступна по пути "C:...\Arduino\Libraries\AAA".
Затем я заливаю скетч на GitHub, и мне придется указать в ReadMe, что пользователь должен скачать библиотеку AAA и корректно ее расположить, чтобы IDE подхватила. Это довольно неудобно и для меня, и, особенно, для пользователя. В случае, если библиотеки расположены в папке скетча, достаточно клонировать репозиторий и все сразу собирается.
Альтернативным решением, принятым в цивилизованном мире, является использование менеджеров зависимостей (привет NuGet, npm, pip и т.д), чтобы прописать зависимости проекта и они потом автоматически подгрузились. Но, увы, среда Arduino не предоставляет таких возможностей.
Я тоже в какой-то момент столкнулся с такой проблемой. Насколько я понимаю, фактически это нарушает всю идею заголовочных файлов. Причина этого в том, как Arduino IDE (если это поделие можно называть IDE) производит обработку файлов. Повозившись немного, понял, что лучшим решением проблемы является переход на использование инструментов, разработанных для разработчиков, а не для домохозаяек.
Лучшим, что нашёл, является platform.io. Порадовало наличие возможности производить тестирование кода, организация сборки под разные платформы, наличие работающей системы зависимостей (в Arduino IDE есть выкидышь на эту тему, целый пакетный менеджер, который не позволяет задавать зависимости проекта, в результате чего периодически сборка чужого проекта превращается в игру «угадай версию библиотеки, использованной разработчиком»). На данный момент меня полностью устраивает. В случае, если будете пользоваться и посмотрите в сторону IDE, рекомендую посмотреть на версию, основанную на VS Code. Как минимум на моём не молодом ноутбуке данная версию работала куда как бодрее, чем выкидышь на Atom.
Platformio поставил, смотрю. Прикольно. Примерно всё то же самое. Редактор (Atom) такой же неудобный. Меня бы больше устроил из командной строки make, в редактор есть Sublime :)
Посмотрю на вс-коре
Главная прелесть planform.io в том, что его можно запускать из командной строки. При этом можно запускать как сборку всех целей (под разные платы, если есть), так и под конкретную. Единственное, что мне показалось не совсем удобным, это способ задания последовательного порта. Но это может быть причиной поверхностного ознакомления с документацией.
Спасибо за ценный совет.
Вспомнил, пользовался platform.io как-то год назад, (правда, на атоме и лагало сильно), но довольно понравилось. Забыл, вот.
Я так-то редко пишу именно под Arduino, а эту свистопляску с библиотеками обнаружил, когда один человек попросил написать для него небольшую вещь. И чтобы его не утруждать, хотел скинуть все библиотеки в проект, чтобы обойтись одним git clone
, условно.
Даже не знаю, какая инструкция проще:
- скачайте такие-то библиотеки
- скачайте platform.io
Начинающим на Arduino: Упаковываем конечный автомат в отдельный класс и библиотеку