Pull to refresh

Comments 59

Круто, всё собирался написать что-нибудь подобное в общем виде для себя.

Один вопрос: virtual-методы заставляют компилятор всунуть в класс указателя на vtable, в конструктор — инициализацию указателя и еще куда-нибудь — саму vtable.

Вы не думали над тем, чтобы реализвовать полиморфизм времени компиляции с помощью CRTP? Результат будет тот же, размер объекта меньше на 1 указатель.
Я лишь описал очевидный подход. Глубоко не копал. Надеюсь, компилятор меня понял и сунул всё по максимуму на флеш :)
Когда начинаешь считать байты, вся красота сходит и получается голый Си и портянка кода в 50 страниц одним куском.

Например, состояния кнопки, можно впихнуть по две кнопки в байт, а верхнего уровня класс по 8 кнопок на байт (по биту) может держать.

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

В примере с кнопкой, я бы предположил, что компилятор сделает ранее связывание и не будет никаких лишних указателей. Можно проверить сделав sizeof у объекта.
Проверил — размер PressButton ровно на 1 байт больше (byte sw), чем у родительского SmartButton.

Беда начинается, если бы были указатели на объекты.
Интрига: а почему 11?

внимание, спойлер
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
Да-да. Вкурил, спасибо.
Утоптал в 6 байтов в итоге, уже лучше.
Размер SmartButton 11 байт, унаследованного PressButton 12.
Я думал про шаблон, да. Я пока не настолько владею плюсами. Я закончил кодить, когда они только появились и были странным глумлением над С.
Мне не нравится, например, что надо конструкторы у нового класса надо писать. Лучше б сгенерить.
CRTP не даст ли доступ к приватным переменным порождённому классу? Это было бы плохо и создаёт потенциальные проблемы.
CRTP не даст ли доступ к приватным переменным порождённому классу?
Нет, конечно. private член доступен только внутри класса. Он может быть переопределен в наследнике, хоть с шаблонами, хоть без (это один из красивых сюрпризов С++), во вызов или доступ к нему возможны только изнутри класса.

Мне не нравится, например, что надо конструкторы у нового класса надо писать. Лучше б сгенерить.
Я не совсем понял, что имеется в виду под «писать».

Если «реализовать» — то это не обязательно, до тех пор пока вас устраивает дефолтная реализация. Дефолтная реализация конструктора по умолчанию не делает ничего, а копирования — вызывает конструктор копирования родителя, а потом конструкторы копирования всех членов класса. Для POD типов (int, float, ..., и struct без конструкторов) — читай копирует побайтно.
Я не совсем понял, что имеется в виду под «писать».

Вот это:
PressButton(byte bt_pin) : SmartButton(bt_pin) {};

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


using SmartButton;
О! Это в каком месте? внутри конструктора наследуемого класса или вместо него?

class newclass: public baseclass {
using baseclass;
или
newclass() { using baseclass; }

вот так:


class newclass: public baseclass {
public:
   using baseclass;
...
};
Спасибо, попробую. Эх, отстал от моды я на 30 лет…
это один из красивых сюрпризов С++

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

Подробнее хорошо описано тут: 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 
}


Это позднее связывание. Фуфу. Не наш метод. Оно как раз подъедает по 4 байта в ардуине на указатель. :)
Если можно обойтись ранним, лучше им.
Это не позднее связываение — все выполняется на этапе компиляции, и никаких 2 байт под указатель там не будет.
Наотимизировал SmartButton и он теперь 6 байт жрёт.
причём 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. Это сильно снижает количество глупых ошибок.
На гитхабе уже enum class
Здесь чем проще, тем лучше. Для начинающих.
Отступ, хмм… не так принципиально ведь?
Отступ, хмм… не так принципиально ведь?

Разумеется, не принципиально, просто странно.
Главное — не доводить до такого
Осторожно, может вызвать сильное глазное кровотечение





(шепотом дилера) Между прочим, а вы слышали про #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 кнопка, знает то что она делает (переключает лед).
Класс кнопки должен описывать только изменение её состояния. А то захочется вам сделать тугл но не для леда и будете городить новый класс

так и есть. городить новый. в это суть подхода. что в этом страшного?

в том что зачем если можно использовать классический Event-Observer в котором кнопка тригерит ивент, а обсервер по "идентфикатору кнопки" выполняет действия. Тогда вы разграничиваете логику и у вас 1 класс тугл.
SOLID

У вас нарушается и прицип инверсии зависимостей, и принцип единственной ответственности

Надо почитать хоть что это.
Я не программист. Про бульдозер я загнул, конечно, но я закончил с программированием промышленным лет 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) Ивент-обсервер:
У вас есть события и обработчики. Событие к примеру "нажатие кнопки", "правый клик мышкой" и тд и тп
Обработчик это объект который "слушает"(ждет) событие и когда оно происходит выполняет какие-то действия
В этом случае ваша кнопка не знает что будет происходить по её нажатию. Условно можете это представлять как реальную кнопку, которая не знает, что будет происходить в цепи когда на неё нажмут, она лишь замыкает или размыкает контакт.
По аналогии вы создали кнопку, которая может зажигать только светодиод. В реальном мире это выглядело бы как кнопка связанная с светодиодом, которая не может быть использованна ни с чем кроме светодиода. Не логично, лучше создать универсальную кнопку)


Чем больше ваши классы связанны между собой тем сложнее будет расширять (масштабировать) систему в будущем.

Я в примере сделал класс «кнопка toggle с подсветкой светодиодом». Это новый вид кнопки.

Это привязывает вас в будущем к определеннию кнопки заново каждый раз с постоянным повторением части логики.
В оптимальном случае вы будете довлять только "что-то новое", то есть обработчик нового события

Хммм… Соглашусь. Да.
Здесь компромисс пока что. В атмеге нет динамической памяти, STL и тп. Памяти вообще мало — 2Кб на данные.
Идею я понял с обсервером, не спешу, но подумаю.

А если по-старорежимному сделать класс toggle с колбеком? Хотя нет, каждый колбек 2 байта жрёт данных, это дорого для кнопочек.
Универсальность да, хочется, согласен.
Покурю про обсерверы ещё.

А по поводу SOLID.
Это 5 принципов, следуя которым, вы в 90% случаев напишите хороший код, который вы сможете легко тестировать, масштабировать и поддерживать в будущем

Да вроде как старался следовать…
Так советовал же CRTP — минус 2 байта на vtable, за счет отказа от виртуальных функций, и никаких указателей на колбеки.
Я помню. Для этого надо сначала освоить новую эту фичу.
Я на плюсах писал в начале 90х или конце 80х, не помню. С тех пор много нового появилось и шаблоны одно из них. Я пока не вкурил тему. Курю.
Звучит странно, но работает. На откуп компилятору.
Arduino IDE позволяет использовать синтаксис C++11, оказывается. То есть, там очень развитый объектно-ориентированный язык.

Совсем не так. Там полноценный С++, но из среды выполнения выпилили исключения и нет всяких STL, ибо на эмбеддед мало памяти и вообще динамическая аллокация — зло.
Надо будет набросать статейку про тюнинг ардуиновского тулчейна…
Давай! Буде статья — дай знать!
Накорябал, жду модерации

Оффтоп про 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.
Начинают же домохозяйки :) и для них Arduino IDE прекрасно во всём :)
Platformio поставил, смотрю. Прикольно. Примерно всё то же самое. Редактор (Atom) такой же неудобный. Меня бы больше устроил из командной строки make, в редактор есть Sublime :)
Посмотрю на вс-коре
Здравствуйте!

Главная прелесть planform.io в том, что его можно запускать из командной строки. При этом можно запускать как сборку всех целей (под разные платы, если есть), так и под конкретную. Единственное, что мне показалось не совсем удобным, это способ задания последовательного порта. Но это может быть причиной поверхностного ознакомления с документацией.
С vs code намного лучше, хмм…
С гитом дружит, ооочень хорошо это.

Я такой минус нашёл:

#include <Wire.h>
#include "ssd1306.h"


Пока явно не указал #include <Wire.h> — не собирался. Подозреваю, что в ssd1306.h вместо <> стоят кавычки :)

Спасибо за ценный совет.


Вспомнил, пользовался platform.io как-то год назад, (правда, на атоме и лагало сильно), но довольно понравилось. Забыл, вот.


Я так-то редко пишу именно под Arduino, а эту свистопляску с библиотеками обнаружил, когда один человек попросил написать для него небольшую вещь. И чтобы его не утруждать, хотел скинуть все библиотеки в проект, чтобы обойтись одним git clone, условно.


Даже не знаю, какая инструкция проще:


  • скачайте такие-то библиотеки
  • скачайте platform.io
Platformio пока нравится. Играю вот.
Sign up to leave a comment.

Articles