Bystre triki w C++ (1): wykorzystanie Reguły Zera do śledzenia wywołań Wielkiej Piątki

Reguła Zera jest szczególnym przypadkiem Reguły Pojedynczej Odpowiedzialności – jeżeli jeden z członków twojej klasy potrzebuje specjalnego traktowania przez Wielką Piątkę (destruktor, operatory przypisania, konstruktory kopiujące i konstruktory przenoszące), to powinien być umieszczony w osobnej klasie, inaczej twoja klasa robi za dużo.

Tradycyjnie gdy chcemy prześledzić gdzie wołany jest operator przypisania, dopisujemy do klasy ten operator, w którym wypisujemy na standardowe wyjście. To podejście ma wady – przy dopisywaniu trzeba sprawdzać czy to nie zmieni działania istniejącej klasy, oraz wymusza sprawdzenie które operatory istnieją a które nie – w przypadku klas które nie mają operatora przypisania, dopisanie takiego operatora może zmienić działanie kodu. Ale z użyciem Reguły Zera kod mógłby wyglądać tak:

class noisy
{
    noisy& operator=(noisy&&) noexcept { std::cout << "operator=(noisy&&)\n"; }
    noisy& operator=(const noisy&) { std::cout << "operator=(const noisy&)\n"; }
    noisy(const noisy&) { std::cout << "noisy(const noisy&)\n"; }
    noisy(noisy&&) noexcept { std::cout << "noisy(noisy&&)\n"; }
    ~noisy() { std::cout << "~noisy()\n"; }
    noisy() { std::cout << "noisy()\n"; }
};

class moja_klasa
{
    noisy n;
    int a;
    std::vector<int> b;
};

Et Voila. Klasę można uogólnić żeby nazwa klasy określonej w parametrze szablonu także była wypisywana.

template<typename T>
class noisy
{
    noisy& operator=(noisy&&) noexcept { std::cout << "operator=(noisy<" << typeid(T).name() << ">&&)\n"; }
    noisy& operator=(const noisy&) { std::cout << "operator=(const noisy<" << typeid(T).name() << ">&)\n"; }
    noisy(const noisy&) { std::cout << "noisy(const noisy<" << typeid(T).name() << ">&)\n"; }
    noisy(noisy&&) noexcept { std::cout << "noisy(noisy<" << typeid(T).name() << ">&&)\n"; }
    ~noisy() { std::cout << "~noisy<" << typeid(T).name() << ">()\n"; }
    noisy() { std::cout << "noisy<" << typeid(T).name() << ">()\n"; }
};

class moja_klasa
{
    noisy<moja_klasa> n;
    int a;
    std::vector<int> b;
};

Pułapki składni języka C, część 1 – dlaczego instrukcja switch musi odejść

Składnia języka C swego czasu była bardzo zwięzła, i wpłynęła mocno na składnie innych języków. Uważam jednak, że po 40 latach można przestać wychwalać ją pod niebiosa i zacząć obiektywnie analizować względem wpływu na ilość bugów w programie.

Instrukcja switch służy do wyboru pomiędzy kilkoma opcjami – najczęściej wartościami typu wyliczeniowego lub stringami – na początku została wprowadzona, żeby umożliwić kompilatorom C implementacje „tablic skoku” dla tego rodzaju instrukcji.

skocz o 8*n bajtów do przodu
wykonaj instrukcje dla 0 (jest to 8 bajtowa instrukcja)
wykonaj instrukcje dla 1 (jest to 8 bajtowa instrukcja)
wykonaj instrukcje dla 2 (jest to 8 bajtowa instrukcja)
wykonaj instrukcje dla 3 (jest to 8 bajtowa instrukcja)

Minęło 40 lat, i sądzę, że kompilatory są wystarczająco bystre by zauważyć podobną optymalizację na drabinkach ifów.

Pierwszym problemem który można zauważyć na pierwszy rzut oka, to jest to, że switch w C działa tylko na typy numeryczne: znaki, liczby, typy wyliczeniowe (nie stringi). Ale to drobnostka, która odwraca uwagę od prawdziwych problemów. (oraz, jest to na tyle oczywiste, że Java i C# naprawili to – ale nie do końca – nie możesz sprawdzić nulla poprzez case null:, i musisz tego switcha otoczyć sprawdzaniem nulla poprzez if(a != null) jak jakiś jaskiniowiec, laffo)

Główny problem z instrukcją switch polega na tym, że tak naprawdę nie jest usprawnieniem notacji drabinki ifów. Z dobrą instrukcją typu switch mógłbym napisać coś takiego

case a {
    1, 2, 3, 4, 5: {
        ++licznik;
    }
    0: {
        print "Błąd";
    }
}
else
{
    print "Nieprawidłowa opcja" 
}

Po przepisaniu tego na C wychodzi coś takiego:

switch(a)
{
    case 1:
    case 2:
    case 3:
    case 4:
    case 5:
        ++licznik;
        break;
    case 0:
        puts("Błąd");
        break;
    default:
        puts("Nieprawidłowa opcja");
        break;
}

W tym momencie można zauważyć, że to tak naprawdę robi się mało czytelne, i można to zastąpić drabinką ifów

if(a >= 1 && a <= 5)
    ++licznik;
else if(a == 0)
    puts("Błąd");
else
    puts("Nieprawidłowa opcja");

Niestety, nie można tego zrobić zawsze. Pierwszy powód jest taki, że switch rzuca błędem gdy mamy podwójnie występujący przypadek. Ponadto, dla enumów jest nas w stanie ostrzec o nieobsłużonym przypadku. Więc

  • nie powinno się wszystkiego zrobić na drabinkach ifów, jak to jest w Pythonie.
  • istnieje wyraźna potrzeba na instrukcję przełączającą która nie ssie.

Ktoś może mi powiedzieć, że czasami przeskakiwanie pomiędzy etykietami („fall-through”) może się przydać, a drabinka if/else nie pozwoli mi na fall-through. Z mojego doświadczenia wynika jednak co innego: w 99% przypadków chcemy wyskoczyć ze switcha, a tylko w 1% przypadków chcemy przeskoczyć dalej. W ten sposób switch faworyzuje mniej używany przypadek na rzecz częściej używanego. Kompletnie nieracjonalna decyzja z punktu widzenia użytkownika języka. (wniosek – C to gloryfikowany asembler)

Dość powiedzieć, że Java i C# uniemożliwiają „fall-through” – właśnie ze względu na bugi. Widać jak Java i C# na ślepo kopiują rzeczy z C bez zastanowienia się. C++ nie miał wyboru, bo jednym z celów projektowych była kompatybilność źródłowa z C, ale zarówno Java, jak i C#, jak i język D nie mają usprawiedliwienia. To co powinni zrobić zamiast wygląda tak (hipotetyczna składnia switcha):

switch(a)
{
    case 0:
        do_this();
    case 1:
        do_that();
        continue;
    default:
        do_the_other_thing();
}

Wiele poradników stylu byłoby zachwyconym tym pomysłem, gdyż nakazują jawne pisanie komentarza gdy robimy fall-through – równoznaczny kod w C wyglądałby tak:

switch(a)
{
    case 0:
        do_this();
        break;
    case 1:
        do_that();
        // fall-through
    default:
        do_the_other_thing();
        break;
}

Ponadto, większość przypadków przeskakiwania obejmuje przypadek, gdy pierwsza etykieta jest pusta, a druga nie, w ten sposób z 1% robi się 0.01% – zamiast:

case 0:
case 1:
case 2:
    do_this();

można byłoby zrobić (styl Pascalowy)

case 0, 1, 2:
    do_this();

albo (gcc ma rozszerzenie które właśnie udostępnia taką składnie w C)

case 0..2:
    do_this();

Kolejny problem ze switchem polega na tym, że używa słowa kluczowego break, blokując jego użycie w pętlach. Poniższy kod jest częsty za każdym razem gdy mamy „pętle zdarzeń” w kodzie C, razem z typem wyliczeniowym dla zdarzenia określającego rodzaj zdarzenia – zatem w WinAPI, SDLu, Allegro i prawie każdej bibliotece C co ma swoją pętlę zdarzeń:

while(al_wait_for_event(queue, &event))
{
    switch(event.type)
    {
        case ALLEGRO_EVENT_KEY_UP:
            /* wyciśnięto klawisz */
        case ALLEGRO_EVENT_KEY_DOWN:
            /* wciśnięto klawisz */
        case ALLEGRO_EVENT_TIMER:
            /* rysuj ekran */
        case ALLEGRO_EVENT_DISPLAY_CLOSE:
            break; // <- buahahahaha, ale jak? break wyskakuje ze switcha, nie z pętli
    }
}

switch ma tą zaletę, że nie muszę pisać wielokrotnie wyrażenia – switch(++skomplikowane_wyrazenie->tablica[indeks]). Całe szczęście w drabincę ifów mogę zrobić to samo:

const auto& t = ++skomplikowane_wyrazenie->tablica[indeks];
if(t == 0)
    do_this();
else if(t == 1)
    do_that();

W C++ dochodzi kolejny problem deklaracji deklaracji zmiennych o nietrywialnym typie.

#include <vector>
#include <iostream>

int main()
{
    int wartosc = 2;
    switch(wartosc)
    {
        case 0:
            std::vector<int> vec = { 1, 2, 3, 4, 5 };
            for(auto& x : vec)
                std::cout << x << " ";
            break;
        case 2:
            std::cout << 5;
            break;
    }
}

Ponieważ switch to praktycznie obliczone goto, z etykietami (w C# możesz nawet zrobić goto case 5!) nie możesz przeskoczyć inicjalizacji, bo konstruktor mógłby się nie wywołać. Więc język to blokuje, i spowoduje wyświetlenie poniższego błędu:

switch_case_failure1.cpp: In function 'int main()':
switch_case_failure1.cpp:13:8: error: jump to case label [-fpermissive]
   case 2:
        ^
switch_case_failure1.cpp:10:21: error:   crosses initialization of 'std::vector<int> vec'
    std::vector<int> vec = { 1, 2, 3, 4, 5 };

Żeby powyższy kod się skompilował, konieczne są dodatkowe nawiasy klamrowe

case 0:
{
    std::vector<int> vec = { 1, 2, 3, 4, 5 };
    for(auto& x : vec)
        std::cout << x << " ";
    break;
}

I tu mam dylemat, i bo nie mam pojęcia jak zrobić tu czytelne wcięcia, i czy lepiej jest wstawić break wewnątrz nawiasów klamrowych, poza nimi, a jak poza nimi, to czy break ma być w osobnej linii czy nie, a jeżeli nie w osobnej linii, to czy zrobić wcięcie dla break czy nie.

Prościej jest to po prostu przerobić na drabinkę ifów. Z wyżej wymienionych powodów nie używam instrukcji switch w swoich programach w ogóle.

Podsumowując, główny problem z instrukcją switch polega na tym, że za każdym razem kiedy myślę, że jest dobry czas na jej użycie, to po jakimś czasie okazuje się, że i tak trzeba ją przerobić na drabinkę ifów.

Hashtag

Ponieważ sporo ludzi nie ogarnia, tu jest krótkie obrazkowe wyjaśnienie:

hashtag

Hash to jest znak #.

Tag (etykieta) to jest system kategoryzacji oparty o nadawanie rzeczom etykiet, gdzie tych etykiet może być zero, jedna lub kilka dla danej rzeczy.

Hashtag to etykieta poprzedzona znakiem #.

Wrażenia z używania Stream API w Javie 8

Ostatnio zapoznałem się z API strumieni wprowadzonego w Javie 8. Wprowadzenie API w stylu funkcjonalnym do języka Java to wspaniała wiadomość – język idzie w dobrym kierunku.

Użycie jest nawet proste – najpierw tworzymy strumień:

Dla kolekcji:

kolekcja.stream()

Dla tablic:

Arrays.stream(tablica)

Dla jednej lub kilku wartości

Stream.of("aaa", "bbb", "ccc") // strumienie obiektów
IntStream.of(4, 5, 7, 42) // strumienie intów
DoubleStream.of(2.5, 4.5) // strumienie doubli

Mamy tu pewien rodzaj duplikacji, wymuszony „specjalnym” traktowaniem typów wbudowanych. Niestety, taka jest rzeczywistość tego języka progamowania i jedyny sposób żeby się pozbyć tej duplikacji wymagałby wprowadzenia ulepszonych generyków. Zauważ że jest 8 value types (boolean, int, long, char, byte, float, double, short), ale strumieni operujących na wartościach jest 4. Podejrzewam że pisanie tych wszystkich klas było dla nich wystarczająco irytujące, a zyski na tyle niewielkie (możesz użyć IntStream dla char i short, DoubleStream dla float), że po prostu sobie darowali.

Po utworzeniu takiego strumienia można na nim wywoływać operacje, które mogą, ale nie muszą zamknąć strumień. Po zamknięciu strumienia strumienia nie można go ponownie otworzyć lub przewinąć na początek – tak jak iteratory które nie są ForwardIteratorami w C++ lub iteratory w Pythonie.

Dzięki temu że operacje nie-terminujące zwracają strumień, można łączyć wywołania w łańcuch, o tak:

List<Integer> l = kolekcja.stream()
                          .filter((x) -> x < 5)
                          .map((x) -> x + 5)
                          .collect(Collectors.toList());

Tutaj mamy przykładową operację na strumieniach, których schemat można przedstawić tak:

  1. Tworzymy strumień
  2. Wołamy kolejne operacje
  3. Zbieramy wyniki

Spora część operacji jest leniwa, dzięki czemu można operować na strumieniach nieskończonych – zupełnie na listach w Haskellu. Nie wszystkie operacje są leniwe – w razie wątpliwości polecam skonsultować się z dokumentacją.

Podstawowe operatory funkcjonalne są – mamy map, reduce i filter. W przypadku map trzeba zwracać uwagę na typ wartości zwracanej, na co się nadziałem na sam start – jeżeli mamy strumień obiektów i to map() musimy użyć z funkcją Object -> Object. Jak mamy funkcje co przyjmuje prymityw i zwrca Object albo na odwrót, to musimy użyć odpowiednich operacji dla tych funkcji.

stream.map(/* funkcja Object -> Object */) // zwraca Stream
stream.mapToInt(/* funkcja Object -> int */) // zwraca IntStream
stream.mapToDouble(/* funkcja Object -> double */) // zwraca DoubleStream
stream.mapToLong(/* funkcja Object -> long */) // zwraca LongStream
int_stream.map(/* funkcja int -> int */) // zwraca IntStream
int_stream.mapToObj(/* funkcja int -> Object */) // zwraca Stream

Nie ma tu za krzty generyczności, o czym świadczy fakt, że te wszystkie klasy ktoś musiał napisać te wszystkie klasy, metody i interfejsy (każda funkcja map* przyjmuje osobny interfejs funkcjonalny). Co gorsza, nie ma metod Stream.mapToObj ani IntStream.mapToInt, więc musisz się też martwić jaki jest strumień po lewej stronie i użyć odpowiedniej nazwy.

Na koniec zbieramy wyniki: na przykład poprzez sum, collect, reduce, count. first. Z tych dwóch na uwagę zasługują collect oraz reduce:

  • reduce jest w dwóch postaciach: dwuargumentowej (T, T -> T -> T) zwracającej T będący redukcją, lub jeżeli jest pusty, lewy argument; oraz jednoargumentowej zwracającej Optional<T>, ponieważ null jest bezużyteczny – nie można na nim wołać metod ani robić cokolwiek sensownego poza porównaniem z innym obiektem.
  • collect to ogólne pojęcie na „zbierz te wszystkie wartości i zwróć mi je w tej postaci”. Mamy metodę collect która przyjmuje trzy operacje: utwórz wartość początkową, dodaj coś, złącz wartości, które dla np. Collections API odpowiadają funkcjom List::new, List::add i List::addAll. Ponieważ te trzy operacje są ściśle połączone, interfejs Collector łączy je w jedną całość jako jeden kolektor który można przekazywać. Gotowe kolektory można znaleźć w java.util.Stream.Collectors:

    • Collectors.toList() – zwraca listę ktora zawiera elementy ze strumienia (nie ma żadnych gwarancji której klasy implementującej List<T> obiekt zostanie zwrócony)
    • Collectors.toCollection() – zwraca kolekcje konkretnej klasy utworzonej przez funkcje podaną jako argument (np. ArrayList<T>::new)

Podsumowując, Java 8 Stream API jest bardzo uniwersalne i będę je używał wszędzie gdzie się da 😛

MSYS2 – jak uczynić programowanie w C++ znośnym na Windowsie, cz. I

Na Windowsie mamy dwie opcje jeżeli chodzi o programowanie w C++: albo Microsoftowe Visual Studio, albo, w stylu linuksowym, MinGW. Pick your poison.

Visual Studio jako środowisko jest ogólnie dobre, ale nie wystarczająco dobre. Konkurencja czyha, a Microsoft śpi, o czym świadczy jak dobrze wspierany jest C++11 w kompilatorze Microsoftu. W zamian za to, mamy dobre środowisko programistyczne razem ze sprawnie działającym debuggerem.

MinGW wywodzi się z Cygwina, którego celem jest emulacja wszystkich aspektów POSIXa na Windowsie. Problem z Cygwinem jest taki, że to overkill. Ponadto, skompilowane programy linkują się z biblioteką cygwin-1.dll, co tworzy problemy licencyjne. Kolejnym problemem jest trudność zarządzania zależnościami. Ludzie instalują każdy pakiet, co mija się z celem.

Więc mamy sobie MinGW, i wydaje się, że wszystkie problemy odeszły? No nie, one dopiero się zaczynają: rozdrobnienie na miliard różnych wersji i odmian MinGW powoduje utrudnienie w dystrybucji bibliotek i pakietów. Jak wiadomo, wszystkie biblioteki C++ muszą być skompilowane tą samą wersją kompilatora z tymi samymi ustawieniami.

Linux rozwiązuje ten problem bardzo prosto: wpisujesz apt-get install nazwa-pakietu i już masz swoją bibliotekę zainstalowaną, w najnowszej wersji.

Na Windowsie albo bezskutecznie szukasz gotowych binarek na swoją wersję kompilatora, albo kompilujesz ze źródeł, co nie jest najłatwiejszą rzeczą na świecie, gdyż wymaga to męczenia się z ./configure, make, a także, w zależności od projektu, innymi silnikami kompilacji (build engines). Ekstra zabawa dochodzi wtedy gdy biblioteka wymaga kompilacji kilku innych bibliotek, które następnie wymagają innych bibliotek itd.

MSYS2 posiada menedżer pakietów pacman z Arch Linuksa do instalacji pakietów, co czyni instalację bibliotek tak łatwą jak w Linuksie. W KOŃCU.

Strona projektu

Ściągnij MSYS2

Opis instalacji

Krótki opis instalacji (w razie wątpliwości lub problemów skonsultuj się z manualem powyżej):

  1. Zainstaluj MSYS2 do katalogu w ścieżce bez spacji (WAŻNE, bo inaczej będziesz miał problemy na każdym kroku)
  2. Uruchom msys2_shell.bat
  3. Zaktualizuj bazę pakietów pacman -Sy
  4. Zaktualizuj główne pakiety pacman -S --needed filesystem msys2-runtime bash libreadline libiconv libarchive libgpgme libcurl pacman ncurses libintl
  5. Zamknij okno konsoli.
  6. Uruchom autorebase.bat.
  7. Uruchom ponownie msys2_shell.bat
  8. Zaktualizuj resztę pakietów pacman -Su
  9. Gotowe! Masz podstawowe środowisko MSYS2. Teraz możesz instalować pozostałe pakiety.

Podstawowe komendy pacmana:

  • Instalacja pakietów: pacman -S nazwa-pakietu
  • Szukanie pakietów: pacman -Ss nazwa-pakietu
  • Usuwanie pakietów: pacman -R nazwa-pakietu
  • Aktualizacja pakietow: pacman -Su
  • Aktualizacja bazy pakietów: pacman -Sy

Reelism – mod do Dooma

Mod do Dooma, którego celem jest przetrwać jak najdłużej w walce z nieustannie pojawiającymi się wrogami. Gra co jedną rundę trwającą 60 sekund losuje rodzaj przeciwników, rodzaj dostępnych broni i inny modyfikator rozrywki (np. zwiększona szybkostrzelność lub wyłączone tarcie). Rozrywka jest bardzo zróżnicowana ze względu na losowość.

Polecam.

Download Reelism pack

  1. Rozpakuj paczkę (7-zip)
  2. (Opcjonalnie) Załatw plik doom2.wad (tj. kup Doom II: Hell on Earth) i użyj go zamiast freedoom.wad.
  3. Uruchom GET_REEL.bat.
  4. (Opcjonalnie) Ustaw odpowiednią rozdzielczość i sterowanie i inne bzdurki.

Remake gry Snake II z Nokii 3410 w wersji alfa

Piszę remake Snake II na Pythona, na jedyny słuszny język do pisania gry w węża :) Jest parę innych już projektów w necie które to robią, ale robią to źle, bo zbyt daleko od oryginału lub mają kilka innych problemów. Moim celem jest emulacja wszystkich aspektów oryginalnej gry, nawet bugów.

Oto jak gra wygląda w tej chwili:

snake_1

Jest możliwość poruszania się wężem, nie ma kolizji, nie ma jedzenia do zjadania.

std::tuple – odpowiedź na zwracanie w stylu C kilku wartości z funkcji

Dość częstym pytaniem które początkujący programista może sobie zadawać, jest „jak mogę zwrócić kilka argumentów z funkcji”? Najczęstszą odpowiedzią jest „użyj struktury/klasy”. I to jest dobra odpowiedź, bo podpowiada takiemu programiście, że może istnieć powiązanie pomiędzy zwróconymi danymi, np. w grze FPS, dla jakiegoś identyfikatora dla broni, chcemy także wyciągnąć wszystkie informacje o ilości naboi w magazynku, ogólnej ilości pocisków, częstotliwości strzelania, mocy pocisku. Wtedy zamiast wyciągać po kolei kolejne informacje, tworzymy obiekt będący reprezentacją broni. Ma to wiele zalet, np. możliwość utworzenia sensownych powiązań pomiędzy różnymi elementami programu, enkapsulację oraz wspomaganie Reguły Jednej Odpowiedzialności (SRP, single responsibility principle)

Nie nadaje się to jednak dla kilku wartości, które są luźno powiązane ze sobą:

div_t wynik = div(5, 2);
obj.wart = wynik.quot;
obj.inna = wynik.rem;

Zauważ, że trzeba wypakować strukturę po jej stworzeniu. Ale da się jeszcze bardziej wygłupić, za pomocą argumentów wyjściowych:

uchar red, green, blue;
Fl::get_color(color, red, green, blue);

Tutaj program wydaje się prostszy… aż do momentu gdy programista chce wstawić te liczby nie do zmiennych typu uchar, lecz do intów. Wtedy jeszcze trzeba podstawić do odpowiednich wartości. Ponadto można łatwo nadziać się tutaj – programiści C bardzo nie lubią takiego kodu – nie widzą czy red, green, blue zmieni zawartość po wywołaniu funkcji – woleliby użycie tutaj wskaźników. (o tym, czy ich obawy są zasadne, to temat na inny wpis). Mamy w sumie ponadto drugi problem – gdyby funkcja przyjmowała te argumenty przez wartość, mielibyśmy tu bezsensowny kod, ale jeszcze mający niezdefiniowane zachowanie – użyto niezainicjalizowanych zmiennych. Ponadto, nie możesz zignorować części danych, musisz je gdzieś wstawić do zmiennych. Generalnie słabizna.

I tu do gry wkracza std::tuple (tuple, po polsku „krotka”), ugólnienie klasy std::pair na dowolną liczbę przechowywanych wartości –

std::tuple<int, int> divmod(int a, int b)
{
    return { a/b, a%b };
}

int main()
{
    auto reszta = divmod(5, 2);
    std::cout << std::get<0>(reszta) << "  " << std::get<1>(reszta); // wypisze: 2  1
}

Możemy też podstawić zwrócone wartości do innych zmiennych:

long wynik;
long reszta;
std::tie(wynik, reszta) = divmod(5, 2);

Albo zignorować część wyniku:

long wynik;
std::tie(wynik, std::ignore) = divmod(5, 2);

To tylko wąski zakres możliwości dla std::tuple. Z std::tuple można by stworzyć „structy 2.0”, czyli nazwane krotki:

struct Osoba : public std::tuple<int, std::string, std::string>
{
    auto wiek() -> decltype((std::get<0>(*this)) { return std::get<0>(*this); }
    auto imie() -> decltype((std::get<1>(*this)) { return std::get<1>(*this); }
    auto nazwisko() -> decltype((std::get<2>(*this)) { return std::get<2>(*this); }
}

W ten sposób można udostępnić zarówno dostęp do pól po nazwach, jak po indeksach. A dzięki indeksom i odrobinie metaprogramowania z szablonami można robić na przykład pętle czasu kompilacji:

Osoba osoba(24, "Jan", "Kowalski");
for_each_tuple_element(osoba, [](auto& x) // auto jako typ argumentu to funkcjonalność C++14
{
    std::cout << x << "\n";
});

Hipokryzja

std::string linia;
std::getline(std::cin, linia);

Czasami widzę opinie, jak to referencje są niebezpieczne, bo mogą ukrywać efekty uboczne, oraz że tutaj std::getline powinno raczej przyjmować wskaźnik, żeby wszyscy wiedzieli, że linia może zmienić swoją zawartość wewnątrz funkcji.

Ponieważ C jest super i nie pozwala na zmianę zawartości argumentów poprzez funkcję, chyba że widać & (najwyraźniej nawet twórca C++ FQA tak uważa)…

TYP x, y;
// ustaw zmienne
z = f(x, y);
// argumenty się nie zmieniły!

Hahahaha, chyba se żartujecie

typedef char TYP[50];
char* f(char* dest, const char* source)
{
    return strcpy(dest, source);
}

To może być uznawane za argument za tym, żeby nie używać typedefów do C-tablic (z czym też się zgadzam), lecz dla mnie to jest syndrom większego problemu z C – tego, że połowa funkcjonalności jest zupełnie nielogiczna i niekonsekwentna (C-tablice zachowują się zupełnie inaczej niż praktycznie każdy inny typ).

Powyższa demonstracja nie unieważnia tezy, że „argumenty wyjściowe to zło”. Niestety, jest to zło konieczne. C++ na szczęście ma dzisiaj wygodniejsze alternatywy (o czym będzie w kolejnych wpisach)

OpenCart, czyli jak zrujnować swoją reputacje w 4 dni.

Cała historia zaczęła się wtedy, gdy użytkownik GitHuba Damian Bushong zwrócił uwagę Danielowi Kerrowi, maintainerowi projektu OpenCart, że jedna z funkcji przeniesiona z kodu źródłowego innego projektu nie wskazuje na oryginalnego autora, łamiąc warunki licencji. Poprawienie tego problemu normalnie by zajęło 5-10 minut, jednak Daniel Kerr stwierdził, że łatwiej jest po prostu odpowiedzieć „fuck off!”. No i rozpoczęła się największa fala czarnego PRu od czasów Ocean Marketing.

Oryginalny wątek był tutaj Historię całego wątku przed serią usuwania komentarzy można zobaczyć tutaj