Lawful Evil #4 – pętla switch

W tym odcinku opiszę problem z wątku zatytułowanego oryginalnie “Pilnie Potrzebna POMOC Program Obliczający Średnią C++”:

Zadanie: Napisz program, który wczytuje w pętli liczby różne od zera i oblicza ich sumę oraz średnią. Wczytanie zera oznacza zakończenie działania pętli i wyświetlenie obliczonych wartości. W C++.
Najlepiej w Dev-C++.
Będę wdzięczny za wszelkie wskazówki.
Czekam na odpowiedź.

Po pewnym czasie autor zmienił treść posta na mniej bezczelną i usunął część komentarzy, ale daję słowo, że nie dostał odpowiedzi w tym stylu bez powodu.

Przechodząc do meritum: ponieważ wymagane było użycie Dev-C++, musiałem ograniczyć się do C++031.

Pętla switch

Aby nie rozwiązywać zadania nazbyt typowo, zamiast pętli while czy for, użyłem pętli switch. Pewnie znajdzie się zaraz jakiś cwaniak, który powie, że switch to nie pętla. Nic bardziej mylnego! Wystarczy tylko wspomóc się poleceniami setjmp i longjmp, i można utworzyć szablon klasy implementującej pętlę switch:

template<typename State>
class Loop
{
    enum ReturnValue{
        FirstIteration = 0,
        ContinueLoop = 1,
        BreakLoop = 2
    };
 
public:
    Loop(State& s) : state_(s) {}
 
    template<typename Body, typename Condition>
    void execute(Body b, Condition c){
        switch(setjmp(loopBuffer_)){
        case FirstIteration:
        case ContinueLoop:
            if(!c(state_)){
                longjmp(loopBuffer_, BreakLoop);
            }else{
                b(state_);
                longjmp(loopBuffer_, ContinueLoop);
            }
        }
    }
 
private:
    State& state_;
    std::jmp_buf loopBuffer_;
};

Gwoli wyjaśnienia: setjmp to makro, które można stosować w bardzo ograniczonych okolicznościach. Pozwala ono na zachowanie kontekstu wywołania funkcji i powrót do niego za pomocą funkcji (tylko w górę stosu wywołań) longjmp. Jest to takie goto na sterydach, ponieważ nie wywołuje destruktorów obiektów o automatycznym czasie życia (kolokwialnie: na stosie).

Gdy setjmp wywoływane jest po raz pierwszy, ewaluuje się jako wartość 0. Gdy wątek wykonania wraca do niego z intrukcji longjmp, to setjmp przyjmuje wartość drugiego argumentu funkcji longjmp. W tym momencie działanie poniższego kodu powinno być dość oczywiste.

template<typename Body, typename Condition>
void execute(Body b, Condition c){
    switch(setjmp(loopBuffer_)){
    case FirstIteration:
    case ContinueLoop:
        if(!c(state_)){
            longjmp(loopBuffer_, BreakLoop);
        }else{
            b(state_);
            longjmp(loopBuffer_, ContinueLoop);
        }
    }
}

Przy pierwszym wywołaniu, lub jeśli longjmp zostało wywołane z ContinueLoop sprawdzany jest warunek pętli. Jeśli jest spełniony – wywoływane jest ciało pętli, a następnie wywoływana jest funkcja longjmp z drugim parametrem ContinueLoop. W przeciwnym wypadku longjmp dostaje parametr BreakLoop.

Uważny czytelnik zauważy, że można ten warunek odwrócić i, gdy warunek nie zostanie spełniony, po prostu puścić wykonanie dalej. Owszem, ale wtedy by było zbyt prosto, a przecież nie o to w tej serii chodzi.

Przygotowanie do użycia pętli switch

Aby użyć pętli switch należy przygotować:

  • klasę opisującą stan pętli,
  • funkcję/funktor sprawdzającą warunek pętli,
  • funkcję/funktor wykonującą ciało pętli.

Naturalnie, parametrem warunku i ciała pętli jest referencja do stanu pętli.

Klasa stanu pętli

Tutaj nie ma nic zaskakującego. Ostatnio wczytana wartość, ilość wczytanych wartości, ich suma.

struct AveragingLoopState
{
    int lastRead;
    int count;
    int sum;
};

Warunek pętli

Zgodnie z zadaniem: “Wczytanie zera oznacza zakończenie działania pętli”. Być może tutaj dałoby się trochę bardziej pokombinować.

bool loopCondition(AveragingLoopState const& s){
    return s.lastRead;
}

Ciało pętli

Wczytaj kolejną wartość. Jeśli wczytywanie zakończono, bądź wpisano zero – nie zwiększaj licznika.

void loopBody(AveragingLoopState& s){
    if(!(cin >> s.lastRead)){ s.lastRead = 0; }
    if(s.lastRead){
        ++s.count;
        s.sum += s.lastRead;
    }
}

Użycie

Pozostaje tylko pokazać implementację użycia pętli switch:

int main()
{
    AveragingLoopState s;
    s.lastRead = -1;
    s.count = 0;
    s.sum = 0;
    Loop<AveragingLoopState> loop(s);
    loop.execute(&loopBody, &loopCondition);
    cout << "Srednia to: " << static_cast<double>(s.sum) / s.count << endl;
}

Myślę, że nie ma tutaj nic odkrywczego.

To by było na tyle. Całość można zobaczyć tutaj. Tym razem mniej więcej wygrało dobro:

Nie działa tak jak powinno, ale dzięki. – Wieczny Student

Przy czym należy temu zaprzeczyć, wykonałem nawet test na VMce z XP:

Wykonanie: Dev-C++ na Windows XPWykonanie: Dev-C++ na Windows XP

1Wiem, że istnieje Orwell Dev-C++ (teraz zresztą projekt chyba porzucony), ale zdecydowana większość zapytań o Dev-C++ oznacza tę prawie pełnoletnią wersję od Bloodshed.

2 thoughts on “Lawful Evil #4 – pętla switch

  1. Dzięki twojemu przykładowi nauczyłem się czegoś (jak działa setjmp i longjmp) dzięki!

Leave a Reply

Your email address will not be published.