Substytut finally w C++

2

Jak sobie radzicie z kodem ktory trzeba wykonac zawsze na koniec bloku? Zeby bylo mniej abstrakcyjnie zalozmy ze w funkcji wykorzystywana jest zmienna ktora zawsze na koniec musi byc inkrementowania. Opcje jakie widze:

  1. Klasyczne RAII - stworzyc osobna klase napisana specjalnie dla konkretnej sytuacji - wedlug mnie pozbawione sensu (zakladajac ze dla konkretnej potrzebnej sytuacji nikt jeszcze nie napisal odpowiedniej klasy).
  2. Kombinowac jak zrobic to samo ale nie wymagajac wolania zawsze na koniec. W tym konkretnym przykladzie inkrementacja mogla by byc na poczatku, a funkcja by operowala na dodatkowej zmiennej. Dla mnie zmiana dzialania programu zeby obejsc problemy jezyka jest jeszcze bardziej bez sensu. I raczej ciezko by cos takiego zrobic np. dla fopen() + fclose().
  3. Recznie wolac kod wielokrotnie przy kazdym wyjsciu z bloku (czyli przed kazdym return i po wyjatku) - a to najgorsza z dotychczasowych opcji.
  4. O dziwo w standardzie znam sposob w ktorym mozna to uzyskac. Zostalo to zaprojektowane do czego innego (choc tez mozliwe ze niekoniecznie. z dokumentacji: "Unlike std::unique_ptr, the deleter of std::shared_ptr is invoked even if the managed pointer is null." - po cos tak zrobili) i wyglada paskudnie, ale dziala:
std::shared_ptr<void> onExit(nullptr, [&](void*) {
		processedCount++;
});
  1. Napisac klase podobnie jak w punkcie 1, ale uniwersalna, np:
struct OnExit {
		OnExit(std::function<void()>&& func) : func(func) { }
		~OnExit() {
			func();
		}
private:
		const std::function<void()> func;
};
...
OnExit onExit([&] {
		processedCount++;
});
  1. Klase powyzej mozna opakowac w makro (wiem ze wielu programistow ma alergie na makra, ale dla mnie jest to najlepsza z tych opcji) i wolac np. tak:
ON_EXIT(
		processedCount++;
)

A moze znacie inne sposoby? A moze w standardzie jest cos rozsadniejszego niz shared_ptr?
Ktoras z powyzszych opcji jest dla was nieakceptowalna bo jest "za brzydka" (a moze sa jakies inne powody)?

7

Wersja 5/6 jest dość szeroko używana, boost ma nawet implementację scope_exit. Od C++17 samodzielne napisanie takiej klasy przyjmującej lambdę to kilka minut roboty.

https://www.boost.org/doc/libs/1_65_1/libs/scope_exit/doc/html/BOOST_SCOPE_EXIT.html

2

ale dlaczego "zmiana dzialania programu" ? przepisujesz program z innego języka czy co?

1

Najbardziej trywialna implementacja generycznego scope guarda wraz z wolną funkcją pomocniczą załatwia jakieś 90% potrzeb, do tego przykładu dodaj lepsze raportowanie błędów i możesz zając się innymi sprawami - https://github.com/pdy/simpleopenssl/blob/master/test/utils.h#L95

Jak potrzebujesz czegoś wymyślniejszego albo wolałbyś przyjemniejszy interfejs to może ScopeGuard z folly - https://github.com/pdy/FollyScopeGuard/blob/master/folly/ScopeGuard.h

0
several napisał(a):

Najbardziej trywialna implementacja generycznego scope guarda wraz z wolną funkcją pomocniczą załatwia jakieś 90% potrzeb, do tego przykładu dodaj lepsze raportowanie błędów i możesz zając się innymi sprawami - https://github.com/pdy/simpleopenssl/blob/master/test/utils.h#L95

To ze to podejscie rozwiazuje problem to wiem. Nie kojarze teraz sytuacji w ktorej kilka linijek powyzej (+ ewentualnie 3 dodatkowe linijki makra) by nie rozwiazaly problemu (no ok, jestem naiwny i zakladam ze w finally nie sa rzucane wyjatki). Ale ze nie kojarze prostego std::finally to czuje podstep. Zdazylo mi sie tez slyszec od innych programistow cos w stylu "nie podoba mi sie bo nie". No ale jak na razie statystyki tego watku sa optymistyczne - 67% odpowiedzi sugeruje ze jednak programistom sie podoba.

2

Ale ze nie kojarze prostego std::finally to czuje podstep

No ale co jest takiego skomplikowanego w RAII? Poza tym std::finally byłoby zdecydowanie mniej eleganckim rozwiązaniem gdyż wiązałoby się to z koniecznością inkludowania czegoś z biblioteki a RAII to efekt definicji języka jako takiego i nie potrzebuje żadnych dodatkowych zależności by z niego korzystać. Sam koncept na którym bazuje RAII jest na tyle elegancki, że aż ciężko uwierzyć, że jest w C++ od samego początku i chyba jako jedyny "stary ficzer" C++ jest wciąż implementowany również w nowoczesnych językach, oczywiście w bardziej odpowiednich dla tych języków formach, ale sam koncept jest ten sam.

0
several napisał(a):

Ale ze nie kojarze prostego std::finally to czuje podstep

No ale co jest takiego skomplikowanego w RAII?

Skomplikowanego - prawie nic. Upierdliwego juz wiecej. Pisanie klasy tylko po to zeby w odpowiednim miejscu zinkrementowac zmienna (jak w przykladzie powyzej) jest niepowazne.
@prawie - nie mozna zapominac o wyjatkach. Np. nietrudno sobie wyobrazic sytuacje w ktorej sprzatanie zwraca obiekt do puli - ot proste push_back(). I juz kod jest niepoprawny bo mozemy dostac bad_alloc. Albo bardziej prawdopodobne w kontekscie finally: close() na jakims strumieniu sluzacym do zapisywania. Strumien moze miec buforowanie i close() wola flush(). A wiec moze rzucic wyjatkiem zeby poinformowac o bledzie zapisu.

Poza tym std::finally byłoby zdecydowanie mniej eleganckim rozwiązaniem gdyż wiązałoby się to z koniecznością inkludowania czegoś z biblioteki

Zupelnie nie widze czemu pojedyncze include ze std mialoby byc mniej eleganckie niz pisanie wlasnej klasy lub zciagniecie biblioteki z ta klasa + napisanie pojedynczego include do napisanego/zciagnietego kodu. Przy takim zalozeniu np. vectora tez mozna by sie pozbyc bo latwo jest napisac.

1

Ogólne podejście to, jak podano wyżej, RAII lub ScopeGuard (własny lub z Boosta), ale wszystko tak naprawdę zależy od konkretnego problemu. Jeżeli chcemy po prostu zliczać bloki to możemy sam licznik opakować w klasę:

#include <iostream>

struct ProcessCounter {
    inline static uint32_t counter = 0;
    ~ProcessCounter() {
        ProcessCounter::counter++;
    }
};

int main() {
    {
        ProcessCounter();
    }
    {
        ProcessCounter();
    }

    std::cout << ProcessCounter::counter << "\n";    
}

A jeżeli potrzebujemy zliczać więcej niż jeden typ bloku to możemy w implementacji zamiast zwykłej statycznej zmiennej trzymać statyczną mapę:

#include <iostream>
#include <map>
#include <string>

struct ProcessCounter {
    static inline std::map<std::string_view, uint32_t> counter;

    ProcessCounter(std::string_view key): key_(key) {
    }

    ~ProcessCounter() {
        auto lb = counter.lower_bound(key_);
        if(lb != counter.end() && !(counter.key_comp()(key_, lb->first))) {
            // key already exists
            lb->second++;
        } else {
            // the key does not exist in the map
            counter.insert(lb, {key_, 1});
        }
    }
private:
    std::string_view key_;
};

int main() {
    {
        ProcessCounter("Foo");
    }
    {
        ProcessCounter("Bar");
    }
    {
        ProcessCounter("Bar");
    }

    std::cout << ProcessCounter::counter["Foo"] << "\n";    
    std::cout << ProcessCounter::counter["Bar"] << "\n";    
}
0

Pisanie klasy tylko po to zeby w odpowiednim miejscu zinkrementowac zmienna (jak w przykladzie powyzej) jest niepowazne.

Podzieliłeś się swoją degradującą opinią zupełnie bez uzasadnienia, w javie widziałem klasy dla dużo większych pierdół. Poza tym, w najczęstszych przypadkach nie musisz jej pisać bo nawet w małych projektach prowadzonych dłużej niż rok jakaś forma scope guarda leży gdzies w helpersach lub utilsach albo używa się jakiejś biblioteki. A nawet jeśli musiałbyś ją napisać to jesteś w stanie to zrobić po pijaku i jedną ręką. A finalnie, jeśli nadal uważasz, że to armata na muchę lub po prostu przerasta Cię napisanie klasy to po prostu zainkrementuj swoją zmienną, nikt Ci przecież za to głowy nie urwie.

Zupelnie nie widze czemu pojedyncze include ze std mialoby byc mniej eleganckie niz pisanie wlasnej klasy lub zciagniecie biblioteki z ta klasa

Bo są projekty, które nie korzystają ze standardowej biblioteki i nie są to odosobnione przypadki. RAII jako cecha/ficzer języka oraz łatwa możliwość użycia wszelakich scope guardów bez konieczności inkludowania i linkowania czegokolwiek to obiektywnie duża zaleta technologii.

Przy takim zalozeniu np. vectora tez mozna by sie pozbyc bo latwo jest napisac

Obecność vectora w bibliotece to żaden argument. Vector to, między innymi, wysoce wyspecjalizowana implementacja scope guarda z dodatkowymi ficzerami. A jest to możliwe bo język posiada możliwość implementowania takowych, rozumiesz w którą stronę płynie zależność?

1

Pisanie klasy tylko po to zeby w odpowiednim miejscu zinkrementowac zmienna (jak w przykladzie powyzej) jest niepowazne.

Można tego wymienić kilka razy tyle.

IMO scope exit/scope guard przydałoby się jako klasa w C++ (ew, tfu, makro, ale z lambdami nie ma takiej potrzeby). Osobiście uważam, że enkodowanie zachowania w typie, oraz takiego finally jak scope exit to różne semantycznie zachowania i język + jego biblioteka powinny pozwalać je wykonać użytkownikowi w sposób niezależny od siebie.

0
eleventeen napisał(a):

Zupelnie nie widze czemu pojedyncze include ze std mialoby byc mniej eleganckie niz pisanie wlasnej klasy lub zciagniecie biblioteki z ta klasa + napisanie pojedynczego include do napisanego/zciagnietego kodu. Przy takim zalozeniu np. vectora tez mozna by sie pozbyc bo latwo jest napisac.

Łatwo? Jesteś pewny, że zdecydowana większość pamiętałaby o fyfnastu konstruktorach, conditional noexcept, exception safety, constexpr, etc. (Nie wspominając o tym, że taki wektor powinien działać nawet jeśli jakiś żartowniś w <T> przeciąży comma-operator)?
Wątpię :P

1 użytkowników online, w tym zalogowanych: 0, gości: 1