Czytelność operatorów alternatywy względem wielu warunków

0

Dla uproszczenia przedstawiam szkielet warunku i pytanie jest takie - która wersja warunku jest lepsza i poprawnie napisana ?

if(lastID.exec()){
        if((lastID.last() && lastID.isValid()) || (!lastID.last() && !lastID.isValid())){
            if(...){
                
            }
            else{
                (...)
            }
        }

czy poniższy warunek ? Dodam tylko, że w tym poniższym dubluję praktycznie te same instrukcje które są w warunku wyżej

if(lastID.exec()){
        if(lastID.last() && lastID.isValid()){
            if(...){
                
            }
            else{
                (...)
            }
        }
        else if(!lastID.last() && !lastID.isValid()){
            if(...){
                
            }
            else{
                (...)
            }
        }
    }

Do podglądu umieszczę pełną wersję, która wygląda jak niżej

void SqlQueryModelLoanStatus::myAddRow()
{
    QSqlQuery lastID, addRecord;
    QString strGetID = "SELECT pIDstatusWypoz FROM tStatusWypoz";
    lastID.prepare(strGetID);

    QString strAddRecord = "INSERT INTO tStatusWypoz (pStatusWypoz) VALUES (:StatusWypozyczenia)";

    if(lastID.exec()){
        if(lastID.last() && lastID.isValid()){
            if(this->pLoanStatus.isEmpty()){
                emit messageOfNumber(1);
            }
            else{
                addRecord.prepare(strAddRecord);
                addRecord.bindValue(":StatusWypozyczenia",this->pLoanStatus);

                addRecord.exec();
                addRecord.clear();

                this->pLoanStatus.clear();

                refresh();

                emit loanStatusIsSet();
                emit messageOfNumber(0);
            }
        }
        else if(!lastID.last() && !lastID.isValid()){
            if(this->pLoanStatus.isEmpty()){
                emit messageOfNumber(1);
            }
            else{
                addRecord.prepare(strAddRecord);
                addRecord.bindValue(":StatusWypozyczenia",this->pLoanStatus);

                addRecord.exec();
                addRecord.clear();

                this->pLoanStatus.clear();

                refresh();

                emit loanStatusIsSet();
                emit messageOfNumber(0);
            }
        }
    }
    else if(!lastID.exec()){
        emit messageOfNumber(-1);
    }
}

Jeżeli ktoś by pytał co to robi, to, to wstawia wiersze z tym, że dodałem poprawkę, która zawiera warunek z negacją, bo gdy tabela była pusta, to nie mogłem wstawić żadnego wiersza.

5

Zwróć uwagę, że (lastID.last() && lastID.isValid()) || (!lastID.last() && !lastID.isValid()) to to samo, co po prostu lastID.last() == lastID.isValid() (o ile nie polegasz na jakichś efektach ubocznych).

Ale z dwojga złego, lepsza wersja pierwsza — jeśli coś dublujesz, to masz dwa razy więcej miejsc do popełnienia błędu, oraz wprowadzasz konieczność synchronizacji potencjalnych zmian, która może nie być oczywista i/lub weryfikowana automatycznie.

0

@Althorion:

Oczywiście, jeśli polegasz na efektach ubocznych ewaluacji

nie rozumiem co chcesz przez to powiedzieć ?

to coś się może rozejść

jak może się rozejść ?

a co do warunku który napisałeś lastID.last() == lastID.isValid() - to rozumiem tak - że jeżeli oba jednocześnie zwrócą prawdę, to wykona się to co poniżej w warunku - a co gdy oba będą fałszem ? Chyba już się nie wykona ?

4

nie rozumiem co chcesz przez to powiedzieć ?

Nie rozumiem, czego nie rozumiesz. Jeśli ewaluacja (sprawdzenie, co zwróci lastID.last() lub lastID.isValid()) ma efekty uboczne (coś gdzieś zmienia) na których polegasz, to krótsza wersja będzie miała inne efekty uboczne, z faktu bycia krótszą. Np. jakbyś miał funkcje:

bool sideEffectExample0() {
  std::cout << "Efekt uboczny!\n";
  return false;
}

bool sideEffectExample1() {
  std::cout << "Efekt uboczny!\n";
  return true;
}

to chociaż logicznie sideEffectExample0() == sideEffectExample1() to to samo, co sideEffectExample1() && sideEffectExample0() || !sideEffectExample0() && !sideEffectExample1(), to ich rezultat będzie inny — mniej razy na standardowym wyjściu się pojawi Efekt uboczny!\n

jak może się rozejść ?

Patrz wyżej. Rezultat logiczny tych wyrażeń jest taki sam. Rezultat programistyczny może być inny, jeśli całość jest bardzo dziwnie zaprogramowana. Wówczas bardzo silnie sugeruję nie programować dziwnie.

a co do warunku który napisałeś lastID.last() == lastID.isValid() - to rozumiem tak - że jeżeli oba jednocześnie zwrócą prawdę, to wykona się to co poniżej w warunku - a co gdy oba będą fałszem ? Chyba już się nie wykona ?

Wtedy też się wykona, bo fałsz jest równy fałszowi.

0

if((lastID.last() && lastID.isValid()) || (!lastID.last() && !lastID.isValid()))
To samo co:
if(!(lastID.last() ^ lastID.isValid()))
Z tym że należy upewnić się że funkcje zwracają bool jeżeli nie to:
if(!((!lastID.last()) ^ (!lastID.isValid())))
ewentualnie:
if(lastID.last() == lastID.isValid())

Parę miesięcy temu radziłem się zapoznać z logiką boolowską ...

6

O ile nie mylę się czytając dokumentację, żaden z tych warunków nie jest poprawny, gdyż jeśli .last() zwróci prawdę, to .isValid() będzie również prawdziwe. I przeciwnie, jak .last() będzie fałszywe, to .isValid() będzie fałszywe, a więc twój warunek się sprowadza do .last() == .isValid(), ale one przecież zawsze są równe! Więc zawsze wchodzisz do ifa, nigdy do else.

Ps. uwaga semantyczna: w pierwszej kolejności warunek powinien być poprawny, dopiero drugim krokiem jest sprawić by wyglądał ładnie/spełniał inne kryteria jakości kodu.

5

Uncle Bob zaleca by każdy warunek w if-ie był własną funkcją z nazwą wyjaśniającą znaczenie.
Czyli tak:

constexpr bool idIsSomeIntrestingDescription(const Id& id)
{
    return (id.last() && id.isValid()) || (!id.last() && !id.isValid()); // pomijam fakt, że warunek dziwny, jak opisał enedil
}


if(idIsSomeIntrestingDescription(lastID)) {

Twój przypadek jest doskonałą okazją by zastosować to podejście.
Uncle Bob robi to zawsze, ale dal mnie to zbyt ekstremalne.

0

@_13th_Dragon:

... WTF? a przy if(warunek) to nikt nie wie? oczywiście nie liczymy tych co nie wiedzą że w nawiasach if() musi być podane wyrażenie o wartości logicznej

pozwól, że zaoram twoją całą mundorść i pewność siebie tym głupim przykładem. Co się wykonało, skoro dla ciebie to takie oczywiste co W DANYM MOMENCIE JEST ZWRACANE ? W przykładzie poniżej jest jasna sprawa, bo mam zmienną typu bool i jasno tą zmienną zainicjalizowałem wartością, którą znasz. A co w przypadku, jeżeli jest to funkcja zwracająca wartość typu bool i nie wiesz jaką wartość logiczną zwróci, a chcesz żeby koniecznie dla tej wartości logicznej warunek się wykonał ?

bool funkcja(){
    std::cout << " w funkcji jest false" << std::endl;

    return false;
}

int main(void)
{
    bool warunek = false;

    if(warunek==false){
        std::cout << "false" << std::endl;
    }
    if(warunek==true){
        std::cout << "true" << std::endl;
    }

    if(funkcja()==true) //funkcja jest wykonana ale warunek się nie wykona
    {
        std::cout << "warunek jest true" << std::endl;
    }
    if(funkcja()==false){ //funkcja jest wykonana i wykona się warunek
        std::cout << "warunek jest false" << std::endl;
    }
}

a teraz weź usuń z warunku wszystkie wartości bool'owskie i spróbuj mi w tej całej swojej przemądrzałej naturze powiedzieć który warunek w jakim momencie się wykona ? Chcesz się pośmiać ? To może pośmiejemy się razem ?

4

a teraz weź usuń z warunku wszystkie wartości bool'owskie

🤷 OK, skoro chcesz. Nie zaszkodzi, w celu edukacyjnym:

bool funkcja(){
    std::cout << " w funkcji jest false" << std::endl;

    return false;
}

int main(void)
{
    bool warunek = false;

    if(!warunek){
        std::cout << "false" << std::endl;
    }
    if(warunek){
        std::cout << "true" << std::endl;
    }

    if(funkcja()) //funkcja jest wykonana ale warunek się nie wykona
    {
        std::cout << "warunek jest true" << std::endl;
    }
    if(!funkcja()){ //funkcja jest wykonana i wykona się warunek
        std::cout << "warunek jest false" << std::endl;
    }
}

Zakładam, że przez „wartości boolowskie” miałeś na myśli „literały logiczne” (true oraz false). O to chodziło cytowanemu przez Ciebie Dragonowi. Jeśli Tobie chodziło o co innego — to napisz o co, to może będzie Ci można pomóc.

Jawne porównania do prawdy czy fałszu nie są „błędami” — w takim sensie, że kompilatorowi to nie przeszkadza i jest to poprawny C(++) — ale są nadmiarowe i niepotrzebne ludziom. Co więcej, wprowadzają zamieszanie — jak widzisz coś takiego w kodzie, to zaczynasz się zastanawiać, czy tam czasem nie zachodzą jakieś dzikie efekty uboczne, które by mogły wymusić takie nietypowe i redundantne opisywanie swoich idei.

Bo if w C(++) działa tak, jak Ci napisał Dragon — ewaluuje wartość logiczną dostarczonego wyrażenia, i jeśli jest ona prawdą, to wykonuje ciało warunku (to w klamrach¹). Innymi słowy — if(warunek) {cośtam;} to w tłumaczeniu na polskawy jeśli warunek, to zrób cośtam. Co jest tym samym, co if(warunek == true) {cośtam;}, jeśli warunek jest prawdą, to zrób cośtam. Nie ma powodu w dobrze napisanym kodzie² pisać tego == true jawnie. A jak ktoś uważa inaczej, to czemu nie zacznie pisać if((warunek == true) == true)? Albo if(((((warunek == true) == true) == true) == true) == true)? Kompilatorowi, wciąż, wszystko jedno³. Ludziom nie.


¹ Tak, wiem że klamry są opcjonalne, i tak naprawdę chodzi o kolejną instrukcję (być może zbiorczą, w klamrach), ale nie chcę mieszać.
² Takim, który nie wykorzystuje jakoś mhrocznie efektów ubocznych i przeciążania operatorów.
³ Chociaż pewnie istnieje limit zagnieżdżeń, po którym się podda przy analizie kodu… 🤔

1

Ktoś całkowicie złośliwy mógłby napisać też

std::cout << (warunek ? "true" : "false");

No ale chyba autor wątku jak mówił zna c++ no i te nieprzydatne algebry boola czy jak te tam nazywać. Chociaż wniosek się nasuwa że chyba nie do końca.

2
zkubinski napisał(a):

pozwól, że zaoram twoją całą mundorść i pewność siebie tym głupim przykładem. Co się wykonało, skoro dla ciebie to takie oczywiste co W DANYM MOMENCIE JEST ZWRACANE ?

Tym co napisałeś zaorałeś wszelkie wątpliwości co do możliwości zaorania tego samego u ciebie w związku z brakiem podmiotu.

zkubinski napisał(a):

W przykładzie poniżej jest jasna sprawa, bo mam zmienną typu bool i jasno tą zmienną zainicjalizowałem wartością, którą znasz. A co w przypadku, jeżeli jest to funkcja zwracająca wartość typu bool i nie wiesz jaką wartość logiczną zwróci, a chcesz żeby koniecznie dla tej wartości logicznej warunek się wykonał ?

  1. Masz złamanie zasady OST w związku z czym ktoś będzie chciał coś dodać/poprawić w twoim kodzie to łątwo o pełnienie błedu
  2. Masz złamanie zasady DRY w związku z czym mniejsza kontrola nad tym co się dzieje
  3. Większość zoptymalizowała by twój kod do:
#include <iostream>
using namespace std;

bool funkcja()
{
    cout<<" w funkcji jest false"<<endl;
    return false;
}

const char *boolToStr(bool value)
{
	const char *str[]={"false","true"};
	return str[value];
}

int main(void)
{
    bool warunek=false;

    cout<<boolToStr(warunek)<<endl;
    cout<<"warunek jest "<<boolToStr(funkcja())<<endl;
    return 0;
}
3

jeżeli rzeczywiście uważasz, że warunek jest ok, to pójdę o krok dalej co w sumie już było wyśmiewane na tym forum - widziałem jak w kodzie zawodowcy stosują nawet takie zapisy if(warunek==true) po to by zwiększyć czytelność i na podstawie tak napisanego warunku, ktoś kto czyta kod, będzie wiedział jaką wartość logiczną warunek ma przyjąć

Pierwsze co należy zrobić to wyzbyć się przekonania, że jak jakiś zawodowiec coś zrobił, to to już automatycznie jest dobra praktyka. Ale do meritum - weźże u licha przestań bronić się przed radami które dajemy. Przestań pisać if (warunek==true), pisz if (warunek). Analogicznie, !warunek zamiast warunek==false, a także funkcja() zamiast funkcja()==true. I to nawet nie jest w mojej opinii coś na jaki temat możesz mieć opinię, w świetle założenia, że (mimo już długiej nauki) jesteś początkującym (i proszę, nie wykłócaj się, że tak nie jest). Nauka programowania, to jest często właśnie konieczność dbania o takie szczegóły. Z czasem się nauczysz. A jak będziesz ignorował sugestie (bez dobrych powodów) to gwarantuję, że twój progres będzie dużo, dużo wolniejszy.

I zwróć uwagę, że wcale tutaj Ciebie nie obrażam. To jest rada ze szczerego serca.

0
#include <iostream>
using namespace std;

bool funkcja()
{
    cout<<" w funkcji jest false"<<endl;
    return false;
}

const char *boolToStr(bool value)
{
	const char *str[]={"false","true"};
	return str[value];
}

int main(void)
{
    bool warunek=false;

    cout<<boolToStr(warunek)<<endl;
    cout<<"warunek jest "<<boolToStr(funkcja())<<endl;
    return 0;
}

pobawiłem się tym kodem i mam prośbę - taaaaaak, jestem noobem, lamerem i co tam chcecie jako totalny noob zadam teraz kilka noobowych pytań - bynajmniej nie chcę aby mi tłumaczył @_13th_Dragon, bo ten to jak zacznie, to najpierw się spuszcza jaki to on the best, a oponent to totalne łajno, a potem jak zacznie wyjaśniać, to jego wyjaśnienie ni w to, ni we w to...

oto pytania.... (tak, jestem pod wrażeniem kodu - bo dla mnie na razie działa magicznie) ale do rzeczy

  1. Kiedyś próbowałem się robić stringi typu char * i iterowałem to potem w pętli i wiem, że iterowany wskaźnik na stringu typu char * musi na końcu takiego stringa napotkać znak NULL. I teraz pytanie, jak wstawić ten znak NULL jeżeli wpisujemy napis z klawiatury, a ten napis ma nieznaną długość ciągu ? Ale UWAGA - nie chcę aby wpisujący wpisywał na końcu stringa ZERO.
  2. Jak to w ogóle działa, że jest sobie const char *str[]={"false","true"}; i w zależności jaką wartość logiczną przyjmie zmienna bool warunek to wyświetla się stosowny napis ? Przecież tam nie ma żadnej pętli ani żadnego wyboru i jak sam z siebie char *str wie że ma wyświetlić stosowny napis ?
  3. Czy koniecznie trzeba char * iterować w pętli aby dojść do znaku NULL ?

QFA jeszcze mnóstwo innych pytań które mi wyparowały. Wyjaśni ktoś to łopatologicznie, na obrazkach, rękami etc ?

EDIT

  1. czy poniższy zapis jest poprawny, mimo, że nie używa znaku tablicy []?
char *str = "napis";
  1. Jak to się dzieje, że wypisując cout << str; wyświetli się cały napis, mimo, że nie iteruję w pętli po wskaźniku ?
2

Ad 1.:
Na czuja. Robisz sobie bufor, wczytujesz do bufora; jak zaczyna brakować, to powiększasz lub robisz kolejny; pod koniec wrzucasz do docelowego cstringa. To było, jest, i będzie, paskudne w użyciu — ot, uroki technologii sprzed półwiecza.

Ad. 2.:
On nic takiego nie wie. Ty tam masz tablicę const cstringów — i tyle. false się rzutuje na 0¹, true na 1². Pod str[0] masz cstringa "false", pod str[1] masz cstringa "true".

Ad 3.:
Nie rozumiem pytania. Tak, jak jest zadane — zależy, co chcesz uzyskać.

Ad 4.:
Jest. Przeczytaj sobie o tym, jak się w C(++) degeneruje tablica do wskaźników, żeby ominąć potencjalne pułapki.

Ad 5.:
Bo std::cout ma odpowiednio przeciążony operator<<, który wie, co z tym zrobić.

Odpowiedzi lakoniczne, bo żeby Ci to wszystko porządnie wytłumaczyć, musiałbym napisać (lub przepisać) ze trzy pierwsze rozdziały podręcznika do C(++), a tego mi się robić nie chce. Silnie polecam przy tak szerokich brakach uzupełnić je systemowo — czytając jakiś dobry podręcznik — zamiast doraźnie, pytaniami na forum.


¹ W C++23 — na 0z
² W C++23 — na 1z

2

Ad 1. Nie. Na końcu cstring'a (aby nie mylić ze string'iem) nie może być NULL'a ponieważ NULL to wskaźnik. Na końcu cstring'a musi być znak \0 lub w "innych pisowniach" '\000' lub '\x00' Wczytywanie:

	char tb[1000];

	cout<<"Podaj napis: ";
	scanf("%999[^\n]",tb);
	cin.get(); // trzeba "zjeść" ten '\n'
	cout<<'<'<<tb<<'>'<<endl;

	cout<<"Podaj napis: ";
	fgets(tb,999,stdin); // uwaga wczytuje z tym '\n'
	cout<<'<'<<tb<<'>'<<endl;

	cout<<"Podaj napis: ";
	char *ptr=tb;
	while(((ptr-tb)<999)&&((*(ptr++)=cin.get())!='\n')) {}
	ptr[-1]=0;// można '\0'
	cout<<'<'<<tb<<'>'<<endl;

Ad 2. Działa dokładnie jak int tb[]={13,666}; tylko że zamiast intów masz const char * czyli stałe wskaźniki na cstring
Ad 3. Nie da się tego zrobić, ponieważ nie istnieje taki znak. Trzeba koniecznie iterować do znaku \0. Ba w jednej tablice znaków można umieścić kilka cstring'ów

const char ThreeRows[]="abc\0def\0ghi\0";
for(const char *ptr=ThreeRows;*ptr;++ptr,cout<<endl) while(*ptr) cout<<*(ptr++);

Ad 4. To nie jest poprawny napis, bo nie możesz "bezkarnie" zmieniać jego zawartości. poprawne zapisy: const char *str="napis"; const char rts[]="napis";
Ad 5. cout << str; to przeciążony operator który z kolei używa pętlę.

Weź kurdyban, podręcznik przeczytaj.

0

@Althorion:

On nic takiego nie wie. Ty tam masz tablicę const cstringów — i tyle. false się rzutuje na 0¹, true na 1². Pod str[0] masz cstringa "false", pod str[1] masz cstringa "true".

czyli po prostu masz na myśli, że false rzutuje się na pierwszy element tablicy, a true na drugi element tablicy ?

1

Nie. To by było… wybitnie dziwne. Mam na myśli dokładnie to, co napisałem. false0, true1. Masz linijkę return str[value]; — kompilator widzi, że chcesz indeksować tablicę (str jest tablicą) przy użyciu value, które jest boolem, więc rzutuje tego boola na size_t, z rezultatem jak wyżej, po czym zwraca odpowiedni element — czyli albo str[0], albo str[1].

Jeszcze raz — jedyne rzutowanie tutaj jest z boola na size_t, żeby nim indeksować tablicę.

2
zkubinski napisał(a):

czyli po prostu masz na myśli, że false rzutuje się na pierwszy element tablicy, a true na drugi element tablicy ?

Zwyczajnie przeanalizuj najpierw ten kod:

int boolToHell(bool value)
{
	int str[]={13,666};
	return str[value];
}

A potem porównaj z oryginałem.

3

@zkubinski: Zrób sobie eksperyment i zastanów się dlaczego wyświetlają się wartości takie, a nie inne:

int arr_int[] = {42, 666};
size_t false_size_t = false;
size_t true_size_t = true;

DBG(0) // dlaczego 0?
DBG(1) // dlaczego 1?
DBG(arr_int[0]) // dlaczego 42?
DBG(arr_int[1]) // dlaczego 666?
DBG(false_size_t) // dlaczego 0?
DBG(true_size_t) // dlaczego 1?
DBG(arr_int[false_size_t]) // dlaczego 42?
DBG(arr_int[true_size_t]) // dlaczego 666?
DBG(arr_int[false]) // dlaczego 42?
DBG(arr_int[true]) // dlaczego 666?

char const* arr_char[] = {"s42", "s666"};
DBG(arr_char[0]) // dlaczego s42?
DBG(arr_char[1]) // dlaczego s666?
DBG(arr_char[false_size_t]) // dlaczego s42?
DBG(arr_char[true_size_t]) // dlaczego s666?
DBG(arr_char[false]) // dlaczego s42?
DBG(arr_char[true]) // dlaczego s666?

https://wandbox.org/permlink/pLcuqMQB5aNXis9g

To są podstawy języka, bujasz się z tym od pół dekady, jak nie więcej.

0
zkubinski napisał(a):

czyli po prostu masz na myśli, że false rzutuje się na pierwszy element tablicy, a true na drugi element tablicy ?

False masz równe 0 i True równe 1, ale kompilator uznaje wszystko większe od zera za True, ale True rzutowane to 1.

Mamy taki fragment kodu arr to wskaźnik, czyli adres gdzie to w pamięci jest.

const char* arr[] = {"abcd", "efgh"};

Tak sobie możesz sprawdzić gdzie to w pamięci jest, adres wirtualny każdy proces ma swój i arr jest na stosie, dla pierwszego i drugiego elementu na stosie.

cout<< (void*)arr <<endl;
cout<< (void*)(arr+1) <<endl;

W tym przykładzie pierwszy i drugi wyłuskany adres z tej tablicy, wskazują na adresy w sekcji .rodata

cout<< (void*)*arr <<endl;
cout<< (void*)*(arr+1) <<endl;

Kolejne wyłuskanie da ci pojedynczą literę.

cout<< **arr << endl;

Pewne implikacje, dereferencja gwiazdką pozwala pobrać dany typ danych z pod danego adresu jeśli masz int* to pobierze ci inta, jeśli char** to adres cstringa, jeśli char* to znak.

arr[0] == *(arr+0) == *arr
arr[1] == *(arr+1)

Teraz czemu jest +0 i +1 jak to nie ma sensu?
A bo kompilator ułatwia prace programiście żeby się nie męczył i to się nazywa arytmetyka na pointerach kompilator wie, że operujesz na procesorze 64 bitowym czyli jeden adres ma wartość 8 bajtów.
I dodając +1 dodajesz całe 8 bajtów i tu jest odpowiednik tego assemblerowy.

cout<< *(const char**)((void*)arr+8) <<endl ;

Rzutuję najpierw na void* pointer żeby oszukać kompilator, żeby nie wiedział jaki typ danych jest i zastosował arytmetykę normalną 1 == 1.
Rzutowanie z powrotem na wskaźnik na wskaźniki char i wyciągamy z tego element, w tym wypadku drugi gdyż jest pierwszy adres na stosie i następne 8 bajtów jest następny, bo tyle w architekturze 64 bitowej ma wielkość adres.

1

Nie istotne jaki rozmiar wskaźnika mamy, nie istotne jaki typ tablicy mamy:

size_t i=someIndex;
someType tb[someSize];

zawsze:

if(&tb[i]==(tb+i)) cout<<"Ofcause!"<<endl;
if((tb+i)==(i+tb)) cout<<"Ofcause!"<<endl;
if((i+tb)==&i[tb]) cout<<"Ofcause!"<<endl;

Natomiast to: cout<< *(const char**)((void*)arr+8) <<endl; nigdy nie ma prawa się skompilować.
Więc nie wiem co chciałeś udowodnic.

2

Panowie to nie ma sensu tu jest opór materii. Wielokrotnie dostawał nazwy książek itd. odmawiał czytania, albo "ja przecież znam".

Polecana literatura jet na forum, w internecie itd. ale żeby nie być goło słownym zarzucam
https://www.learncpp.com/

Tu nie przemówicie gościowi do rozsądku, spojrzałem w jego posty. modelami w Qt itp. zaczynał w... 2016 brak progresu. Ja mówię żeby sobie odpuścił i tyle.

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