Random w C++... Z przeciążeniem i szablonami :-) część 2

Tu poruszamy tematy związane z pisaniem programów w języku C++ dla AVR.
Awatar użytkownika
mokrowski
User
User
Posty: 190
Rejestracja: czwartek 08 paź 2015, 20:50
Lokalizacja: Tam gdzie Centymetro

Random w C++... Z przeciążeniem i szablonami :-) część 2

Postautor: mokrowski » piątek 20 paź 2017, 01:16

Na początku wywiążę się z obietnicy. Obiecałem że do kodu z 1 części dodam konstruktory przypisujące dane do obiektu Random tak, aby ułatwić kreowanie i używanie obiektów Random.

random.hpp:

Kod: Zaznacz cały

#ifndef RANDOM_HPP_
#define RANDOM_HPP_

#include <stdint.h>

class Random {
public:

   // Konstruktor domyślny
   Random() { }

   // Konstruktor inicjujący generator
   Random(uint16_t data ): data(rngState) {
   }
   // Zwrot wartości z generatora
   operator unsigned() const {
      update();
      return rngState >> 8;
   }

   // Ustawienie zasiewu generatora
   Random& operator =(const uint16_t& seed) {
      rngState = seed;
      return *this;
   }

private:
   // Definicja stanu generatora
   static uint16_t rngState;

   // Uaktualnienie stanu generatora
   static inline void update() {
      // 16-bit LFSR: x^16 + x^14 + x^13 + x^11.
      // Generuje sekwencję liczb o długości 65535.
      rngState = (rngState >> 1) ^ (-(rngState & 1) & 0xb400);
   }
};

uint16_t Random::rngState = 0x21;

#endif /* RANDOM_HPP_ */


main.cpp:

Kod: Zaznacz cały

#include <avr/io.h>
#include <util/delay.h>
#include "random.hpp"

int main(void) {
   // Ustawienie wszystkich bitów portu C jako wyjście.
   DDRC = 0xFF;

   // Deklaracja zmiennej typu Random
   Random myRandom;

   // Ustawienie zasiewu generatora
   myRandom = 0x21;

   while(true) {
      PORTC = myRandom;
      _delay_ms(1000);
   }
}


W pliku random.hpp, zaimplementowałem konstruktor domyślny Random() {} ,,nicnierobiący” oraz konstruktor z przypisaniem Random(uint16_t data) {... } który przypisuje stan generatora.

Czas przejść do szablonów.

Czy nie było by przyjemne generowanie w locie (przez kompilator) obiektów które obsługiwały by generator dla oznaczonej ilości bitów? Np. dla 8-bitów długości rejestru i 16-bitów długości rejestru. To ostatnie właściwie już jest, ale 8-bitowej wersji nie ma.

Aby to zrobić, powinienem „stempletyzować kod” (to nieformalna nazwa). Czyli zmienić jego implementację na szablonową. Będę mógł wtedy kreować Random w main.cpp np. tak:

Kod: Zaznacz cały

...
Random<8> myRandom = 0x42;
...

Dokładniej takie wywołanie dokona kreacji obiektu na podstawie szablonu i wywoła jego konstruktor z argumentem.

Tu mała uwaga. Mamy „dwie warstwy rzeczywistości”:
  • Czas tworzenia kodu programu na podstawie szablonów.
  • Czas wykonania programu na naszym MCU.
Szablony składają kod pilnując typów i składni języka C++. Makra nie wiedzą nic o składni języka. Oczywiście nie znaczy to że nie należy używać makr w C++! Należy mieć tylko świadomość do czego służą i w jakim celu stosować należy szablony.
Kompilator przetwarzając szablony, może wykonywać bardzo złożoną pracę (nawet algorytmy!) aby wytworzyć kod który później będzie skompilowany. Będzie to kod optymalny dla danych warunków przekazanych w argumentach szablony i bardzo wydajny. Kod taki w niczym nie ustępuje temu co można zrobić w języku C a daje możliwość traktowania programowania w sposób obiektowy i tzw. generyczny (inna nazwa uogólniony).

Na obydwu ,,warstwach rzeczywistości” można programować. Warstwa programowania 1 nazywana jest metaprogramowaniem, ze względu na to że program (szablony) tworzy program :-)
Wbrew straszącej nazwie nie jest to metafizyka. W tym tutorialu nie będę jednak zajmował się metaprogramowaniem w C++.

No to zaczynamy „templetyzować kod” :-)

Przed Random w random.hpp, pojawi się słowo template. W argumentach przekazanych w nawiasach ostrokątnych ( < > ), przyjmie argument w postaci zmiennej int. Nie ma obawy że int będzie 16,32, czy 64 bitowy. Nie jest to istotne bo tu nie jest generowany kod binarny a jedynie źródło kodu. W zasadzie argument ten będzie służył do wyboru sposobu implementacji i będzie zawierał ilość bitów rejestru LSFR.

Będę musiał pamiętać także o zmiennej rngState inicjowanej pod klasą i ją także opatrzyć szablonem.

Zmieni się także wywołanie konstrukcji Random w main.cpp.

W random.hpp:

Kod: Zaznacz cały

...
template<int bits>
class Random {
...
};

template<int bits>
uint16_t Random<bits>::rngState = 0x21;
...

W main.cpp:

Kod: Zaznacz cały

...
   // Deklaracja zmiennej typu Random
   Random<16> myRandom;
...

Na razie kod nie robi nic więcej niż poprzednio. Zwróć jedynie uwagę na przypisanie w random.hpp:

uint16_t Random<bits>::rngState = 0x21;

Argument szablonu trafia do nazwy Random bo kompilator ma wiedzieć jak dostać się do atrybutu aby go przypisać.

Wracamy do definiowania szablonu.

Jeśli będę chciał mieć Radndom dla 8 bitowego rejestru LSFR, to powinienem zmienić implementację:
1. update() - tak aby wykonywał operację na 8 bitach.
2. operator unsigned() const – tak aby zwracał 8 bitów młodszych bo starszych ... nie będzie :-)

Uważni dostrzegą jeszcze że warto jest mieć wersję konstruktora Random(uint8_t data) { .. } tak aby przypisywał wartość rngState no i sama rngState powinna być typu uint8_t a nie uint16_t jak to było pierwotnie.
Te elementy jednak teraz pominę bo w 16 bitach na pewno zmieści się 8 bitów. Problemem typów zajmę się innym razem (może powstanie cz. 3 ? :-) ).

Aby zmusić kompilator do generowania wyspecjalizowanego kodu z szablonu, stosujemy .. specjalizację szablonu :-)

W pliku random.hpp, dodam specjalizację dla wersji 8 bitowej.

Kod: Zaznacz cały

...
// Specjalizacja dla wersji 8-bitowej generatora
template<>
void Random<8>::update() {
   // 8-bit LFSR: x^8 + x^6 + x^5 + x^4.
   // Generuje sekwencję liczb o długości 65535.
   rngState  = ( (rngState >> 1) ^ (-(rngState & 1) & 0x00B8)) & 0xFF;
}

// Specjalizacja dla wersji 8 bitowej generatora
template<>
Random<8>::operator unsigned() const {
   update();
   return static_cast<uint8_t>(rngState);
}
...

Specjalizacja szablonu to w przypadku Random podanie szablonu bez argumentów z wypełnionym argumentem przy definicji samego typu (w tym przypadku <8>). Kompilator otrzymując wartość bits w argumencie Random<TU> (w main.cpp), natknie się na konieczność specjalnego potraktowania definicji metody update() oraz operator unsigned() const. Po wstawieniu takiego kodu do źródła przed kompilacją, mamy specjalną, wydajną, wspaniałą (dodać własne zachwyty..) metodę dla 8-bitowych rejestrów LFSR :-)

Wspaniale. Mamy specjalizację dla 8-bitowych i... ogólny kod dla całej reszty. Hmm... Możemy
więc użyć Random<8>, Random<16>... ale (niestety) także Random<32> (i tu błędu kompilator nie zgłosi a jedynie kod będzie zawierał rejestr 16-bitowy) czy Random<3> (i tu rejestr także będzie 16-bitowy bez zgłoszenia problemu przez kompilator).

No tak, jeszcze „straszna konstrukcja” static_cast<uint8_t>(rngState) :-) To rzutowanie. Wiem że paskudnie wygląda i ma tak wyglądać w C++! W podejściu obiektowym rzutowanie bardzo często świadczy o niedostatkach implementacji (moja ma jeszcze niedostatki) albo o nieprawidłowych operacjach na zbyt niskim poziomie (tak tu operuję na niskim poziomie).
Jest jeszcze jeden powód dlaczego stosuję takie rzutowania. Łatwiej je znaleźć w kodzie niż tradycyjne z C (C++ także je wspiera ale kontroluje tak restrykcyjnie jak C) :-)

Zanim przejdę dalej, stan pliku random.hpp:

Kod: Zaznacz cały

#ifndef RANDOM_HPP_
#define RANDOM_HPP_

#include <stdint.h>

template<int bits>
class Random {
public:

   // Konstruktor domyślny
   Random() { }

   // Konstruktor inicjujący generator
   Random(uint16_t data ): data(rngState) {
   }
   // Zwrot wartości z generatora
   operator unsigned() const {
      update();
      return rngState >> 8;
   }

   // Ustawienie zasiewu generatora
   Random& operator =(const uint16_t& seed) {
      rngState = seed;
      return *this;
   }

private:
   // Definicja stanu generatora
   static uint16_t rngState;

   // Uaktualnienie stanu generatora
   static inline void update() {
      // 16-bit LFSR: x^16 + x^14 + x^13 + x^11.
      // Generuje sekwencję liczb o długości 65535.
      rngState = (rngState >> 1) ^ (-(rngState & 1) & 0xB400);
   }
};

template<int bits>
uint16_t Random<bits>::rngState = 0x21;


// Specjalizacja dla wersji 8-bitowej generatora
template<>
void Random<8>::update() {
   // 8-bit LFSR: x^8 + x^6 + x^5 + x^4.
   // Generuje sekwencję liczb o długości 65535.
   rngState  = ( (rngState >> 1) ^ (-(rngState & 1) & 0x00B8)) & 0xFF;
}

// Specjalizacja dla wersji 8 bitowej generatora
template<>
Random<8>::operator unsigned() const {
   update();
   return static_cast<uint8_t>(rngState);
}

#endif /* RANDOM_HPP_ */

Stoję więc przed wyborem. Czy tak to zostawić, czy zrobić coś jeszcze? Ambitnie było by obsłużyć rejestry do długości 32-bitów. Nie przesadzajmy jednak w tutorialu. Zwróć uwagę że dla każdej z długości rejestru, pojawiają się magiczne wartości bitów XOR'owanych. Ograniczę się więc do maksymalnej długości rejestru 16-bit.

Następne pytanie które należy sobie postawić to: Co dokładnie zmienia się w kodzie po zaimplementowaniu rejestrów od 8 do 16 bitów? W mojej implementacji, będzie to zmiana XOR'owanej maski (w kodzie do tej pory: 0x00B8 dla 8-bit i oraz 0xB400 dla 16-bit) oraz sama maska (w kodzie: maska 0xFF dla 8-bit i brak maski dla 16-bit).

Potrzebujemy więc struktury która poda w zależności od argumentu bits w template, odpowiedni zestaw danych. Nie może to być jednak funkcja/metoda, bo pracujemy w „warstwie rzeczywistości meta” :-). Zakoduję więc strukturę pomocniczą która będzie zawierała dla wszystkich argumentów bits, dane dla LFSR 16-bitowego, a dla bits == 8 dane dla 8-bitowego LFSR.

Kod: Zaznacz cały

...
// Struktura przechowująca wartości XOR'owanych bitów oraz maskę dla Random
template<int bist>
struct RandBitValues {
   static const uint16_t randMask = 0xFFFF;
   static const uint16_t randBits = 0xB400;
};

// Specjalizacja dla 8-bit Random
template<>
struct RandBitValues<8> {
   static const uint16_t randMask = 0x00FF;
   static const uint16_t randBits = 0x00B8;
};
...

Definicje te powinny pojawić się przed definicją Random w pliku random.hpp.

Słowo o zapisie. Jak widać wszystkie atrybuty struktury są statyczne i stałe. To bardzo ważne. Teraz w szablonie bez problemu dostanę się do maski i wartości do XOR'owania. Począwszy od standardu C++11, dostępne jest także słowo kluczowe constexpr. Jeszcze dokładniej precyzuje ono intencje programisty.

W Random::update() będzie to proste bo implementacja będzie wyglądała tak:

Kod: Zaznacz cały

...
   // Uaktualnienie stanu generatora
   static inline void update() {
      // 16-bit LFSR: x^16 + x^14 + x^13 + x^11.
      // Generuje sekwencję liczb o długości 65535.
      rngState = ((rngState >> 1)
            ^ (-(rngState & 1) & RandBitValues<bits>::randBits))
            & RandBitValues<bits>::randMask;
   }
...

Zaraz zaraz, a czy w takim razie, przy założeniach i kodzie który istnieje... specjalizacja update() dla 8-bitów jest konieczna? Odpowiedź brzmi nie :-) No to sio z nią :-)
A specjalizacja dla operator unsigned() const dla 8-bit? Konieczna. No... tu na razie konieczna. W przypadku ogólnym (czyli 16-bitowy rejestr) będziemy mieli pobranie starszego bajtu, a w przypadku szczególnym (argument template 8-bit) wartość młodszego bajtu.

Ale przecież operator unsigned() const zwraca rngState przesunięte w prawo o ... zależne od bits z argumentu template :-) Bingo! Wyrzucamy także i specjalizację dla operator unsigned() i poprawiamy definicję na:

Kod: Zaznacz cały

...
// Klasa Random
template<int bits>
class Random {
...
   // Zwrot wartości z generatora
   operator unsigned() const {
      update();
      return rngState >> ( bits > 8 ? (bits - 8) : 0);
   }
...

Teraz tylko trochę klepania w specjalizacjach RandBitValues. Podanie maski i magicznej wartości dla rejestru LFSR o długościach od 2 do 16 i mamy bardzo reużywalny kod dla szerokich zastosowań Random!

Co można jeszcze? A wpisałem w TODO w kodzie :-) Niektóre z tych zadań wymagają jednak dość zaawansowanych mechanizmów szablonowych (np. wyboru konkretnej imlementacji z szablonu czy operowania na typach).

Na koniec jeszcze stan plików main.cpp i random.hpp, ...

main.cpp:

Kod: Zaznacz cały

#include <avr/io.h>
#include <util/delay.h>
#include "random.hpp"

int main(void) {
   // Ustawienie wszystkich bitów portu C jako wyjście.
   DDRC = 0xFF;

   // Deklaracja zmiennej typu Random
   Random<8> myRandom;

   // Ustawienie zasiewu generatora
   myRandom = 0x21;

   while(true) {
      PORTC = myRandom;
      _delay_ms(1000);
   }
}


random.hpp:

Kod: Zaznacz cały

#ifndef RANDOM_HPP_
#define RANDOM_HPP_

#include <stdint.h>
//
//TODO:
// - wykonać specjalizację dla bits == 0 i 1 jako puste. LFSR dla tych wartości
//   nie ma sensu :)
// - poprawić implementację tak aby bits <= 8, mapowany był na uint8_t rndState
// - dodać automatyczne określanie maski w RandBitValues na podstawie bits
//

// Struktura przechowująca wartości XOR'owanych bitów oraz maskę dla Random
template<int bist>
struct RandBitValues {
   static const uint16_t randMask = 0xFFFF;
   static const uint16_t randBits = 0xB400;
};

// TODO: Tu specjalizacje dla rejestrów od 2 do 16.

// Specjalizacja dla 8-bit Random
template<>
struct RandBitValues<8> {
   static const uint16_t randMask = 0x00FF;
   static const uint16_t randBits = 0x00B8;
};


// Klasa Random
template<int bits>
class Random {
public:

   // Konstruktor domyślny
   Random() { }

   // Konstruktor inicjujący generator
   Random(uint16_t data ) {
      rngState = data;
   }
   // Zwrot wartości z generatora
   operator unsigned() const {
      update();
      return rngState >> ( bits > 8 ? (bits - 8) : 0);
   }

   // Ustawienie zasiewu generatora
   Random& operator =(const uint16_t& seed) {
      rngState = seed;
      return *this;
   }

private:
   // Definicja stanu generatora
   static uint16_t rngState;

   // Uaktualnienie stanu generatora
   static inline void update() {
      // 16-bit LFSR: x^16 + x^14 + x^13 + x^11.
      // Generuje sekwencję liczb o długości 65535.
      rngState = ((rngState >> 1)
            ^ (-(rngState & 1) & RandBitValues<bits>::randBits))
            & RandBitValues<bits>::randMask;
   }
};

template<int bits>
uint16_t Random<bits>::rngState = 0x21;

#endif /* RANDOM_HPP_ */

.. i małe podsumowanie.

Tworząc kod w C++ dla MCU, trzeba wybrać element który będzie łączył w sobie pewną funkcjonalność. Dla ATmega będzie to np. Adc (dla przetworników) Comparator, Timer, Port....
W ramach niego implementujemy proste funkcjonalności poprzez metody. Usprawiedliwione przeciążamy lub ukrywamy w private lub protected.
Jeśli to konieczne, kod staramy się tak uogólnić, aby później o nim ... zapomnieć :-) .. i wyłącznie używać.
Z racji szczupłości zasobów sprzętowych w MCU, kod w C++ będzie bardzo silnie bazował na generowaniu optymalnych implementacji czyli na szablonach.

No to trochę delikatnej argumentacji, bo z premedytacją chcę wywołać dyskusję choć tzw. „trollowanie” mi nie w głowie :-)

Zastanów się przez chwilę jak zaimplementował byś pokazaną funkcjonalność w C...
Makra – tylko gdzie kontrola typów!
Funkcje – czyli specjalizowany update() dla zadanej długości LFSR, ale będzie ich całe stado!
Struktury – tak... w C to substytut klasy :-)

Ceny sprzętu spadają. W niebyt odchodzi ostatni argument na korzyść C na MCU: „Stosuję C bo to jedyny kompilator na mojej platformie”.

C++ to język obiektowy, ale dziś bardziej liczy się że da się programować w sposób uogólniony. Nie wprowadza narzutu a w wielu przypadkach jest tak efektywny (lub nawet bardziej) jak C. Zwróć uwagę że nawet Java Mobile Edition została przez rynek wyparta bo smartfony obsłużą już „grube języki programowania”).
Dzięki automatycznym wyborom specjalizacji kodu, jest nieprzeciętnie elastyczny!
Niestety/Na szczęście aby wycisnąć „siódme poty” z C++, trzeba posługiwać się szablonami :-)
,,Myślenie nie jest łatwe, ale można się do niego przyzwyczaić" - Alan Alexander Milne: Kubuś Puchatek

Wróć do „Programowanie AVR w C++”

Kto jest online

Użytkownicy przeglądający to forum: Obecnie na forum nie ma żadnego zarejestrowanego użytkownika i 4 gości