Pokazywanie postów oznaczonych etykietą GoingNative 2013. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą GoingNative 2013. Pokaż wszystkie posty

27 września 2015

[clang] Dynamiczna i statyczna analiza programów

Coś co chciałem wypróbować już bardzo dawno temu, a mianowicie narzędzie do statycznej i dynamicznej analizy kodu/programów. Zachęcił mnie do tego przede wszystkim wykład z GoginNative 2013: The Care and Feeding of C++’s Dragons (0:51h, 1:05h). Najwyraźniej funkcjonalność którą dostarcza kompilator (clang), została wydzielona do osobnego narzędzia jakim jest scan-build, choć nie eksperymentowałem z tym rozwiązaniem za dużo. Kilka przydatnych linków:

Clang Static Analyzer

Narzędzie korzysta ze zbioru reguł, które wciąż się rozrastają. Można skorzystać z domyślnych ustawień lub samodzielnie wskazać pod jakim kątem ma być przeskanowany kod. Przykład z dzieleniem przez zero, zaczerpnięty z dokumentacji.
#include <iostream>

using namespace std;

int test(int z) {
    if (z == 0)
        return 1 / z;
    return -1;
}

int main() {
    int i = 63;
    std::cin >> i;
    test(i);
    return 0;
}
W pierwszych dwóch linijkach dwa alternatywne wywołania:
clang++ -std=c++14 --analyze -Xanalyzer -analyzer-output=text main.cpp
# inna wersja
clang++ -std=c++14 --analyze -Xanalyzer -analyzer-checker=core.DivideZero -Xanalyzer -analyzer-output=text main.cpp
Wynik:
main.cpp:7:18: warning: Division by zero
        return 1 / z;
                 ^
main.cpp:14:10: note: Passing value via 1st parameter 'z'
    test(i);
         ^
main.cpp:14:5: note: Calling 'test'
    test(i);
    ^~~~~~~
main.cpp:6:9: note: Assuming 'z' is equal to 0
    if (z == 0)
        ^~~~~~
main.cpp:6:5: note: Taking true branch
    if (z == 0)
    ^
main.cpp:7:18: note: Division by zero
        return 1 / z;
               ~~^~~
1 warning generated.
Niestety przykłady które stworzyłem samodzielnie, już nie podają takich wyników. Testy przeprowadzałem na wersji 3.6.0 oraz 3.8.0. No nic, rozwiązanie i tak jest interesujące, liczę że w przyszłości narzędzie się rozwinie.
#include <iostream>

using namespace std;

int test2(int z) {
    return 1 / z;
}

int test3(int z) {
    if (z > 15)
        return 1;
    return 1 / z;
}

int main() {
    int i = 63;
    std::cin >> i;

    test2(i);
    test3(i);

    return 0;
}

Sanitizer

Innym narzędziem, które istnieje w clang-u jest sanitizer, a właściwie ich kolekcja. Analiza przeprowadzana jest dynamicznie, więc najlepsze efekty można uzyskać, jeżeli kod był tworzony razem z testami. Sprawdziłem jedynie AddressSanitizer, ale w dokumentacji można odnaleźć informacji o innych:
#include <iostream>

using namespace std;

int main() {
    char tab1[5];
    for(int i = 0; i <= 5; ++i)
        tab1[i] = 'x';
    return 0;
}

Wywołanie:
clang++ -std=c++14 -fsanitize=address main.cpp
./a.out
Rezultat:
=================================================================
==5748==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffd47e300b5 at pc 0x0000004dc5ad bp 0x7ffd47e30010 sp 0x7ffd47e30008
WRITE of size 1 at 0x7ffd47e300b5 thread T0
    #0 0x4dc5ac  (/home/beru/synetizer_test/a.out+0x4dc5ac)
    #1 0x7fcefcca4a3f  (/lib/x86_64-linux-gnu/libc.so.6+0x20a3f)
    #2 0x4350e8  (/home/beru/synetizer_test/a.out+0x4350e8)

Address 0x7ffd47e300b5 is located in stack of thread T0 at offset 53 in frame
    #0 0x4dc32f  (/home/beru/synetizer_test/a.out+0x4dc32f)

  This frame has 3 object(s):
    [32, 36) ''
    [48, 53) 'tab1' <== Memory access at offset 53 overflows this variable
    [80, 84) 'i'
HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext
      (longjmp and C++ exceptions *are* supported)
Shadow bytes around the buggy address:
  0x100028fbdfc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100028fbdfd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100028fbdfe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100028fbdff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100028fbe000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x100028fbe010: f1 f1 f1 f1 04 f2[05]f2 f2 f2 04 f3 00 00 00 00
  0x100028fbe020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100028fbe030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100028fbe040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100028fbe050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100028fbe060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Heap right redzone:      fb
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack partial redzone:   f4
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==5748==ABORTING

25 grudnia 2014

clang-modernize

Ciekawe narzędzie przedstawione przez Chandlera Carrutha na GoingNative 2013:
Jego głównym zadaniem jest przerobienie istniejącego kodu, wprowadzając nowe mechanizmy ze standardu C++11. Nie jest tego wiele, ale pokazuje, jakie moce drzemią w samym clang-u. Testy przeprowadzałem na wersji 3.5
clang-modernize -add-override \
                -loop-convert \
                -pass-by-value \
                -replace-auto_ptr \
                -use-auto \
                -use-nullptr \
                cpp_modernize.cpp
Niestety nie udało mi się zmusić do działania opcji "pass-by-value", które ma pozwolić na zadziałanie move sementic.
#include <iostream>
#include <memory>
#include <vector>
#include <map>

class ICar {
public:
    virtual ~ICar() {};
    virtual int engine(const std::string& name) = 0;
};

class Car : public ICar {
public:
-   virtual int engine(const std::string& name) {
+   virtual int engine(const std::string& name) override {
        std::cout << "Engine " << name << " start" << std::endl;
        return 44;
    }
};

std::vector<std::string> rewrite(const std::map<std::string, int> m) {
    std::vector<std::string> out;
-   for (std::map<std::string, int>::const_iterator it = m.cbegin(); it != m.cend(); ++it) {
+   for (auto it = m.cbegin(); it != m.cend(); ++it) {
        out.push_back(it->first);
    }

    return out;
}

int main() {
    std::map<std::string, int> m = {{"3", 3}, {"4", 4}, {"8", 8}};
    std::vector<std::string> v = rewrite(m);
-   std::auto_ptr<ICar> ferrari(new Car());
+   std::unique_ptr<ICar> ferrari(new Car());

    int oil_counter = 0;
-   for (int i = 0; i < v.size(); ++i) {
-       if (ferrari.get() != NULL) {
-           int result = ferrari->engine(v[i]);
+   for (auto & elem : v) {
+       if (ferrari.get() != nullptr) {
+           int result = ferrari->engine(elem);
            oil_counter += result;
        }
    }
    return 0;
}

6 sierpnia 2014

clang-format

clang-format.py umożliwia zmianę formatowania pliku (C++) według przyjętej w danym projekcie konwencji. Pokrótce omówił to narzędzie Chandler Carruth w swoim wykładzie na GoingNative 2013 (25 minuta):
Potrafi on korzystać z kilku wbudowanych styli: Google, WebKit, llvm. Możliwe jest także skorzystanie z własnego, za pomocą przełącznika -style=file. Styl taki zostanie zaczytany z pliku .clang-format, który musi znajdować się gdzieś w ścieżce powyżej wywołania. Własny styl można zbudować, na podstawie już istniejącego, najpierw zrzucając oryginał (--dump-config) go pliku, a następnie modyfikując.
# użyj wbudowanego stylu
clang-format-3.5 -style=WebKit -i main.cpp

# zrzuć styl Google do pliku .clang-format
clang-format-3.5 -style=Google --dump-config > .clang-format

# skorzystaj z własnego stylu
clang-format-3.5 -style=file -i main.cpp
Działanie formatera, można zautomatyzować np. w QtCreatorze. Jedna uwaga, wszelkie modyfikacje w:
"Tools | Options... | Environment | External Tools | Add | Add Tool", kasują skrót klawiaturowy, który został podpięty dla takiego formatera.

18 lutego 2014

[C++11] Copy control i move semantic

Kolejny zbiór nowości w standardzie, z którymi nie do końca czuje się jeszcze pewnie. Myślę, że do tematu jeszcze powrócę, jak tylko stanie się on dla mnie bardziej klarowny.

Copy control


Konstruktor kopiujący

W przypadku konstruktora kopiującego parametr jest prawie zawsze referencją na const. Rzadko zdarza się sytuacja, abyśmy byli zmuszeni do modyfikacji obiektu, z którego kopiujemy.
struct Foo {
    Foo(const Foo&);
};

Zasada trzech/pięciu/zero (the rule of three/five/zero).

Istnieją trzy (pięć - w nowym standardzie) podstawowe operacje kontrolujące kopiowanie obiektu:
  • konstruktor kopiujący
  • operator przypisania
  • destruktor
  • move konstruktor
  • operator przeniesienia (move)
Nie ma wymagań by tworzyć wszystkie z nich, jednak powinny być one traktowane jako całość. Bardzo rzadko zdarza się potrzeba stosowania tylko jednej konstrukcji z pominięciem reszty. Sam Stroustrup zaleca by przy deklaracji chociaż jednej z operacji, stworzyć wszystkie pięć. W przeciwnym razie klasa nie powinna zawierać żadnej z tych konstrukcji - "the rule of zero".
Jeżeli nie chcemy użyć któregoś z mechanizmów, poniżej wymieniono kilka zasad, którymi należy się kierować:
  • Jeśli klasa wymaga destruktora, prawie na pewno wymaga konstruktora kopiującego i operatora przypisania
  • Jeśli klasa wymaga konstruktora kopiującego, prawie na pewno wymaga operatora przypisania i vice versa
  • Klasa która nie może być kopiowana powinna definiować konstruktor kopiujący i operator przypisania jako delete, zamiast tworzyć te dwie definicje prywatne
Tworząc operator przypisania należy pamiętać o dwóch rzeczach:
  • Operator przypisania (ma to też zastosowanie dla operatora move) musi pracować poprawnie, gdy próbujemy przypisać do siebie ten same element. Ma to szczególne znaczenie jeżeli, obiekt posiada dane zapisane w pamięci dynamicznej, a w wyniku przypisania tworzymy w pamięci nowe dane i kasujemy stare. W takim przypadku dobrą praktyką jest skopiowanie wartości po prawej stronie do zmiennej lokalnej, zniszczenie wartości stojącej po lewej i w końcu przypisanie jej wartości tymczasowej. [C++Primer, s. 512; s. 536]
  • Większość operatorów przypisania współpracuje z destruktorem i konstruktorem kopiującym
Storage& operator=(const Storage& r) {
    auto tmp = new string(*r.data);
    delete data;
    data = tmp;

    return *this;
}
W przeciwieństwie do mechanizmów copy-control swap nigdy nie jest wymagany, jednakże zdefiniowanie własnej metody, może być ważną optymalizacją dla klasy, która alokuje zasoby. Operatory przypisania, które korzystają z techniki "copy and swap", są automatycznie odporne na wyjątki i obsługują samoprzypisanie.
Należy pamiętać tylko o jednej rzeczy. Nigdy nie wolno wołać bezpośrednio std::swap(). Wymusza to bowiem korzystanie z wersji bibliotecznej swap(), tymczasem klasa może mieć zmienną, która posiada własną wersję tej funkcji. Najlepiej by wyboru dokonał kompilator, przez wciągnięcie przestrzeni nazw.
inline void swap(Storage &l, Storage& r) {
    using std::swap;
    swap(l.data, r.data);
    swap(l.counter, r.counter);
}

void swap(Counter &l, Counter &r) {
    Clock *tmp = l.clock;
    l.clock = r.clock;
    r.clock = l.clock;
}
Jeżeli klasa posiada chociaż jeden z mechanizmów tj.: konstruktor kopiujący, operator przypisania lub destruktor kompilator nie stworzy nam konstruktora i operatora move!

Move semantic


Najtrudniejsza rzecz, jaka przyszła mi do opanowania wraz z nowym standardem, czyli move semantic. Kilka linków:
Ponieważ kompilatory mogą optymalizować tworzenie zmiennych tymczasowych (copy elision), które służą tylko do zainicjowania innych zmiennych wyłączyłem tą opcję.
g++ -std=c++11 -fno-elide-constructors main.cpp

std::move i std::forward

Ciekawie o nich opowiedział Scott Meyers w swoim wykładzie An Effective C++11/14 Sampler na GoingNative 2013, z czego sporo zaczerpnąłem. Żadne z mechanizmów niczego nie obiecuje, ich zadaniem jest jedynie rzutowanie (podczas kompilacji) do rvalue:
  • std::move (rzutowanie bezwarunkowe) - przekazuje obiekt jako rvalue
  • std::forward (rzutowanie warunkowe) - przekazuje obiekt jako rvalue, tylko jeżeli oryginalny obiekt był rvalue inaczej będzie to lvalue
std::move nie gwarantuje tego że coś zostanie przesunięte. Np. jego działanie na obiekcie const spowoduje jego skopiowanie. Korzystając z tego mechanizmu trzeba być bardzo ostrożnym ponieważ kompilator raczej nie poinformuje nas gdy std::move nie będzie przesuwać ("rozświetliło" by to bibliotekę standardową).

Korzystając z std::move obiecujemy, że nie będziemy korzystać już z danego obiektu chyba, że zamierzamy do niego coś przypisać lub go zniszczyć. Po wykonaniu std::move nie mamy żadnej gwarancji co do wartości przesuniętego obiektu.

Głównym zadaniem std::forward jest przekazanie argumentu (rvalue, lvalue, z const-em lub nie) w niezmienionej formie, do innej funkcji. Jeśli oryginalny obiekt, który jest wstawiony do std::forward był rvalue, zrobi z niego rvalue, a jeżeli lvalue, to lvalue - dlatego właśnie jest warunkowy.
void process(Storage& lvalue) {
    cout << "By lvalue reference" << endl;
}

void process(Storage&& rvalue) {
    cout << "By rvalue reference" << endl;
}

template <typename T>
void log(T&& param) {
    process(std::forward<T>(param));
}

int main() {
    Storage st;
    log(st);                     // przekazanie jako lvalue
    log<Storage>(std::move(st)); // przekazanie jako rvalue
}
Wynik:
Default constr.:  defaultData
By lvalue reference
By rvalue reference
Destructor:       defaultData

"Jeżeli oczekujesz szybkiego działania przekazuj przez wartość"

Zalecenia do poprzednich wersji standardu sugerowały by lvalue były opatrzone modyfikatorem const. Jest to już nieprawdą, bowiem powodowało by to kopiowanie obiektu. Jeżeli chcemy coś przesunąć, const nam w tym przeszkodzi.
void process(const Storage st) {
    Storage my = std::move(st); // Kopiowanie!
}

int main() {
    Storage st;
    process(st);
}
Wynik
Default constr.:  defaultData
Copy constructor: defaultData
Copy constructor: defaultData
Destructor:       defaultData
Destructor:       defaultData
Destructor:       defaultData

noexcept dla operacji move

Ponieważ z założenia operacje "move konstruktora" i "move operatora" kradną zasoby i nie alokują żadnych nowych, więc nie powinny też rzucać żadnych wyjątków. Powinniśmy poinformować o tym kompilator stosując noexcept. Co więcej, kontenery takie jak std::vector, sprawdzają, czy "move konstruktor" jest noexcept (podczas realokacji), jeżeli nie, zostanie zawołany zwykły konstruktor kopiujący.
#include <iostream>
#include <string>
#include <vector>

using namespace std;

struct Verbose {
    Verbose(std::string) { cout << "Constructor" << endl; }
    Verbose(const Verbose&) { cout << "Copy" << endl; }
    Verbose(Verbose &&) /*noexcept*/ {
        cout << "Move" << endl;
    }
    Verbose& operator =(const Verbose&) { cout << "assign=" << endl; }
    Verbose& operator =(Verbose&&) noexcept { cout << "move=" << endl; }
    ~Verbose() noexcept { cout << "Destructor" << endl; }
};

int main() {
    std::vector<Verbose> v;
    v.emplace_back("aaa");
    v.emplace_back("bbb");
}
Wynik:
Constructor
Constructor
Copy
Destructor
Destructor
Destructor

Uboczne działania destruktora

Move semantic niesie jeszcze jedno zwodnicze działanie, o którym należy pamiętać. Nigdy nie mamy pewności kiedy zostanie zawołany destruktor obiektu, z którego przenosimy zasoby. Jeżeli zdefiniowaliśmy własny destruktor, który ma jakieś działania uboczne, należy pamiętać o odpaleniu tego samego kodu w konstruktorze i operatorze move.

Przykłady


Pora na przykład, który pokazuje różne scenariusze, z którymi mamy do czynienia kopiując bądź przesuwając obiekty. Na początku "gadatliwa" klasa, która pokazuje, który z mechanizmów copy-control został użyty.
#include <iostream>
#include <string>
#include <vector>
#include <functional>

using namespace std;

struct Storage {
    Storage() : data("defaultData") {
        cout << "Default constr.:  " << data << endl;
    }
    Storage(std::string str) : data(str) {
        cout << "Constructor:      " << data << endl;
    }
    Storage(const Storage& st) : data(st.data) {
        cout << "Copy constructor: " << st.data << endl;
    }
    Storage(Storage&& st) noexcept : data(std::move(st.data)) {
        st.data = "old " + data + ", data was moved from here";
        cout << "Move constructor: " << data << endl;
    }
    Storage& operator=(const Storage& st) {
        if (&st != this) {
            cout << "operator= copy:   " << data << " := " << st.data << endl;
            data = "old " + data + ", now assigned " + st.data;
        } else {
            cout << "operator= copy:   " <<  data << " (this)" << endl;
        }
        return *this;
    }
    Storage& operator=(Storage&& st) noexcept {
        if (&st != this) {
            cout << "operator= move:   " << data << " := " << st.data << endl;
            string old_copy = data;
            data = std::move(st.data);
            st.data = "old " + data + ", data was moved from here";
            data = "old " + old_copy + ", now assigned " + data;
        } else {
            cout << "operator= move:   " << data << " (this)" << endl;
        }
        return *this;
    }
    ~Storage() noexcept {
        cout << "Destructor:       " << data << endl;
    }

    std::string data;
};
W przykładach simpleExample5 i simpleExample6, rzutujemy (std::move) wartość do rvalue reference - jest to referencja, więc stare dane zostają na miejscu (nic nie jest przesuwane, ani kopiowane).
void simpleExample1() {
    Storage st("val_1");
    st = st;
}

void simpleExample2() {
    Storage st1("val_1");
    Storage st2("val_2");
    st1 = st2;
}

void simpleExample3() {
    Storage st1("val_1");
    Storage st2("val_2");
    st1 = std::move(st2);
}

void simpleExample4() {
    Storage st1("val_1");
    Storage st = std::move(st1);
}

void simpleExample5() {
    Storage st1("val_1");
    Storage&& st = std::move(st1);
}

void simpleExample6() {
    Storage&& st1 = std::move(Storage("val_1"));
}

int main() {
    cout << "simpleExample1():" << endl;
    simpleExample1();
    cout << "simpleExample2():" << endl;
    simpleExample2();
    cout << "simpleExample3():" << endl;
    simpleExample3();
    cout << "simpleExample4():" << endl;
    simpleExample4();
    cout << "simpleExample5():" << endl;
    simpleExample5();
    cout << "simpleExample6():" << endl;
    simpleExample6();
}
Wynik dla simpleExample*:
simpleExample1():
Constructor:      val_1
operator= copy:   val_1 (this)
Destructor:       val_1

simpleExample2():
Constructor:      val_1
Constructor:      val_2
operator= copy:   val_1 := val_2
Destructor:       val_2
Destructor:       old val_1, now assigned val_2

simpleExample3():
Constructor:      val_1
Constructor:      val_2
operator= move:   val_1 := val_2
Destructor:       old val_2, data was moved from here
Destructor:       old val_1, now assigned val_2

simpleExample4():
Constructor:      val_1
Move constructor: val_1
Destructor:       val_1
Destructor:       old val_1, data was moved from here

simpleExample5():
Constructor:      val_1
Destructor:       val_1

simpleExample6():
Constructor:      val_1
Destructor:       val_1
W przykładzie returnValue1, kompilator sam się domyśli, że stworzona lokalnie (w ret()) zmienna zaraz zostanie zniszczona, więc zamiast korzystać z operatora przypisania (jak to było dawnej) zoptymalizuje tą operację przesuwając stworzoną zmienną lokalną. Ponieważ wyłączona jest optymalizacja "copy elision", widać dwa przesunięcia. Pierwsze przesuwa lokalną zmienną do zmiennej na stosie, drugie ze zmiennej na stosie do docelowej zmiennej st.

W drugim przypadku (returnValue2), mamy podobną sytuację, z tą różnicą, że zamiast "move operatora" działa "move konstruktor". Gdyby "copy elision" było włączone, kompilator prawdopodobnie wyciągnąłby wywołanie z tak prostej funkcji jak ret() i wywołał po prostu konstruktor w ciele naszego przykładu.
Storage ret() {
    Storage tmp("local_val");
    return tmp;
}

void returnValue1() {
    Storage st("val_1");
    st = ret();
}

void returnValue2() {
    Storage st = ret();
}

int main() {
    cout << "returnValue1():" << endl;
    returnValue1();
    cout << "returnValue2():" << endl;
    returnValue2();
}
Wyniki dla returnValue*:
returnValue1():
Constructor:      val_1
Constructor:      local_val
Move constructor: local_val
Destructor:       old local_val, data was moved from here
operator= move:   val_1 := local_val
Destructor:       old local_val, data was moved from here
Destructor:       old val_1, now assigned local_val

returnValue2():
Constructor:      local_val
Move constructor: local_val
Destructor:       old local_val, data was moved from here
Move constructor: local_val
Destructor:       old local_val, data was moved from here
Destructor:       local_val
Ostatni przykład pokazuje, że do funkcji można przekazać parametr korzystając z std::move, a więc niejako "prosząc" o skorzystanie z mechanizmu move semantic. Gdyby parametr był oznaczony modyfikatorem const, nie było by to możliwe.
Storage set(Storage st_set) {
    return st_set;
}

void settingValue1() {
    Storage st1("val_1");
    Storage st = set(st1);
}

void settingValue2() {
    Storage st = set(Storage("val_1"));
}

void settingValue3() {
    Storage st1("val_1");
    Storage st = set(std::move(st1));
}

int main() {
    cout << "settingValue1():" << endl;
    settingValue1();
    cout << "settingValue2():" << endl;
    settingValue2();
    cout << "settingValue3():" << endl;
    settingValue3();
}
Wyniki dla settingValue*.
settingValue1() - "copy elision" OFF:
Constructor:      val_1
Copy constructor: val_1
Move constructor: val_1
Move constructor: val_1
Destructor:       old val_1, data was moved from here
Destructor:       old val_1, data was moved from here
Destructor:       val_1
Destructor:       val_1

settingValue1() - "copy elision" ON:
Constructor:      val_1
Copy constructor: val_1
Move constructor: val_1
Destructor:       old val_1, data was moved from here
Destructor:       val_1
Destructor:       val_1

settingValue2():
Constructor:      val_1
Move constructor: val_1
Move constructor: val_1
Move constructor: val_1
Destructor:       old val_1, data was moved from here
Destructor:       old val_1, data was moved from here
Destructor:       old val_1, data was moved from here
Destructor:       val_1

settingValue3():
Constructor:      val_1
Move constructor: val_1
Move constructor: val_1
Move constructor: val_1
Destructor:       old val_1, data was moved from here
Destructor:       old val_1, data was moved from here
Destructor:       val_1
Destructor:       old val_1, data was moved from here
Do funkcji pass możemy przekazać tylko rvalue reference (przez rzutowanie std::move), wszelkie próby wsadzenia parametru lvalue ("jeśli zmienna ma nazwę to jest lvalue") zakończą się błędem. Taki parametr najczęściej pojawia się w metodach bibliotecznych i informuje, że możemy ukraść zasoby ze zmiennej. Towarzyszy jej bliźniacza metoda (przyjmująca const T&) będąca wersją służącą do kopiowania zasobów.
Storage pass(Storage&& st) {
    return st;
}

void passingValue1() {
    Storage st1("val_1");
    Storage st = pass(std::move(st1));
}

int main() {
    cout << "passingValue1():" << endl;
    passingValue1();
}
Wynik dla passingValue*:
passingValue1():
Constructor:      val_1
Copy constructor: val_1
Move constructor: val_1
Destructor:       old val_1, data was moved from here
Destructor:       val_1
Destructor:       val_1
Próba zdefiniowania własnego konstruktora kopiującego, operatora przypisania albo destruktora kończy się tym, że kompilator nie wygeneruje za nas operatora i konstruktora move.
struct StorageDestructor : public Storage {
    ~StorageDestructor() noexcept {
        cout << "Own destructor:   " << data << endl;
    }
};

void deriveWithDestructor1() {
    StorageDestructor st1;
    StorageDestructor st2;
    st1 = std::move(st2);
}

int main() {
    cout << "deriveWithDestructor1():" << endl;
    deriveWithDestructor1();
}
Wynik dla deriveWithDestructor1:
deriveWithDestructor1():
Default constr.:  defaultData
Default constr.:  defaultData
operator= copy:   defaultData := defaultData
Own destructor:   defaultData
Destructor:       defaultData
Own destructor:   old defaultData, now assigned defaultData
Destructor:       old defaultData, now assigned defaultData
Kolejny przykład. Choć klasa przez zdefiniowanie własnego destruktora zapobiega stworzeniu (implicit) konstruktora i operatora move, nadal mamy możliwość wymusić ich powstanie przez skorzystanie z default.
struct StorageWithDefaultMove : public Storage {
    // Destruktor zapobiega niejawnemu wygenerowaniu operatora move
    ~StorageWithDefaultMove() noexcept { }
    // Wymuszamy wygenerowania operatora move!
    StorageWithDefaultMove& operator=(StorageWithDefaultMove&&) = default;
};

void dervieWithOwnMoveAssign1() {
    StorageWithDefaultMove st1;
    StorageWithDefaultMove st2;
    st1 = std::move(st2);
}

int main() {
    cout << "dervieWithOwnMoveAssign1():" << endl;
    dervieWithOwnMoveAssign1();
}
Wynik dla dervieWithOwnMoveAssign1:
dervieWithOwnMoveAssign1():
Default constr.:  defaultData
Default constr.:  defaultData
operator= move:   defaultData := defaultData
Destructor:       old defaultData, data was moved from here
Destructor:       old defaultData, now assigned defaultData

Reference qualifier


Nowe definicje i mechanizmy wstecznej kompatybilności sprawiły, że biblioteka standardowa pozwala teraz na przypisywanie do rvalue! Czasami takie użycie może być zaskakujące. Poniższy kod jest prawidłowy.
string s1;
string s2;
s1 + s1 = "Hello world!";
Możemy się przed tym ustrzec w naszych klasach, zmuszając by operand lewostronny (obiekt, na który wskazuje this), był lvalue. Pozwala na to reference qualifier (& dla lvalue, oraz && dla rvlaue), który stosuje się tak jak const, gdy chcemy oznaczyć metody, które nie mogą modyfikować pól obiektu.

Tak jak const, kwalifikatory te można stosować dla funkcji nie statycznych, muszą też znajdować się w deklaracji i definicji, a jeżeli występują razem z const zapisuje się je na końcu. Jeżeli definiujemy dwie lub więcej metod, które mają takie same nazwy i listę parametrów, musimy wprowadzić reference qualifier dla nich wszystkich, albo dla żadnej.

Przykład. Utworzona został operator=, ale tylko dla lvalue. Dodatkowo stworzone zostały dwie metody info, wołające różny kod w zależności od tego na jakim obiekcie zostały zawołane.
#include <iostream>
using namespace std;

class Foo {
public:
    Foo operator+(const Foo &) {
        return *this;
    }
    Foo &operator=(const Foo &) & {
        return *this;
    }

    void info() const & {
        cout << "info: lvalue" << endl;
    }
    void info() && {
        cout << "info: rvalue" << endl;
    }
};

Foo retFoo() {
    return Foo();
}

int main() {
    Foo f1;
    Foo f2;
    Foo f3;

//  (f1 + f2) = f3;     // Error - zapis do rvalue, ale brak takiego operatora
//  retFoo() = f3;      // Error - zapis do rvalue, ale brak takiego operatora

    f1.info();
    retFoo().info();
}
Wynik:
info: lvalue
info: rvalue

1 grudnia 2013

[C++11] Wyjątki i noexcept

Jeżeli wyjątek wystąpi w konstruktorze, nawet jeżeli obiekt jest tylko częściowo stworzony standard gwarantuje, że stworzone już dotąd obiekty zostaną poprawnie zniszczone. Tak samo w przypadku gdy wyjątek wystąpi w momencie inicjalizacji tablicy albo kontenera.

Jeżeli destruktor działa na operacji, która może zniszczyć obiekt, powinien on z-wrapować taką operację w blok try i obsłużyć wyjątek lokalnie. Destruktory nie powinny rzucać wyjątków! Jeżeli podczas zwijania stosu destruktor rzuci wyjątkiem którego sam nie złapał program zostanie zterminowany.
#include <iostream>
using namespace std;

struct MyClass {
    ~MyClass() /* noexcept */ {
        throw std::exception();
    }
};

int main() {
    MyClass a;
    return 0;
}
Wynik:
terminate called after throwing an instance of 'std::exception'
  what():  std::exception
The program has unexpectedly finished.
Kilka zasad dotyczących tworzenia, rzucania i łapania wyjątków:
  • Obiekty wyjątków, które są rzucane, muszą mieć dostępny destruktor, a także konstruktor kopiujący lub konstruktor move.
  • Najbardziej wyspecjalizowany catch musi pojawić się jako pierwszy.
  • Aby wyrzucić wyjątek wyżej, trzeba podać throw bez wyrażenia (throw;).
  • Aby złapać wyjątek każdego typu trzeba skorzystać z catch(...), który powinien zawsze pojawiać się jako ostatni.
#include <iostream>
#include <typeinfo>
#include <stdexcept>
using namespace std;

void old() {
    throw std::runtime_error("old()");
}

void young() {
    try {
        old();
    } catch(const std::bad_cast& e) {
        cout << e.what() << endl;
    } catch(...) {
        cout << "unknown, rethrow" << endl;
        throw;
    }
}

int main() {
    try {
        young();
    } catch(const std::runtime_error& e) {
        cout << "in main: " << e.what() << endl;
    }

    return 0;
}
Wynik:
unknown, rethrow
in main: old()

Lista inicjalizacyjna konstruktora

Konstruktor nie może złapać wyjątków rzuconych z listy inicjalizacyjnej. Aby się przed tym ustrzec, blok try musi znajdować się przed dwukropkiem. Zachowanie jest podobne w działaniu do zwykłych try-catch, z tą różnicą że złapany wyjątek zostanie ponownie wyrzucony z bloku.
#include <iostream>
#include <stdexcept>
using namespace std;

struct Member {
    Member() { }
    Member(Member& m) {
        throw std::runtime_error("Constructor");
    }
};

struct BadCalss {
    Member member;
    BadCalss(Member& m) try : member(m) {
        // body
    } catch(...) {
        cout << "some bug" << endl;
    }
};

int main() {
    Member m;
    BadCalss bc(m);
    return 0;
}
Wynik:
some bug
terminate called after throwing an instance of 'std::runtime_error'
  what():  Constructor
The program has unexpectedly finished.

noexcept

W nowym standardzie pojawił się specjalny specyfikator noexcept. Jeżeli wiemy, że funkcja nie będzie rzucać wyjątku, może z tej informacji skorzystać i programista i kompilator. Wspominał o tym również Scott Meyers w swoim wykładzie na Going Native 2013. Kompilator w takim przypadku wygeneruje znacznie mniej kodu, który będzie bardziej optymalny w działaniu.
#include <iostream>
#include <stdexcept>
using namespace std;

void fun() noexcept {
    throw std::runtime_error("fun()");
}

// gun() ma taki sam specyfikator rzucanych typów jak fun()
void gun() noexcept(noexcept(fun())) {
    cout << "gun()" << endl;
}

int main() {
    fun();
    return 0;
}
Wynik:
terminate called after throwing an instance of 'std::runtime_error'
  what():  fun()
The program has unexpectedly finished.
W rezultacie noexpect powinno być stosowane w dwóch przypadkach. Kiedy jesteśmy przekonani, że funkcja nie rzuci wyjątku albo/i gdy nie chcemy obsługiwać błędu. Jeżeli z funkcji (oznaczonej jako noexcept) zostanie jednak rzucony wyjątek, natychmiast zawołane zostanie std::terminate().

Inny przykład, w którym wyjątek przelatuje przez funkcję z noexcept, co powoduje zterminowanie programu.
#include <iostream>
#include <stdexcept>

using namespace std;


void fun_a() {
    throw std::runtime_error("bug");
}

void fun_b() noexcept {
    fun_a();
}

void fun_c() {
    try {
        fun_b();
    } catch(...) {
        cout << "Exception caught" << endl;
    }
}

int main() {
    fun_c();
}
Wynik:
terminate called after throwing an instance of 'std::runtime_error'
  what():  bug
The program has unexpectedly finished.
Niektóre kontenery jak np. std::vector, sprawdzają (podczas realokacji wektora), czy "move konstruktor" jest oznaczony jako noexcept. Jeżeli tak nie jest zostanie zwołany zwykły konstruktor kopiujący, co jest operacją dużo wolniejszą. Odpowiedni eksperyment stworzyłem w innym artykule.
Poza "move konstruktorem" noexcept standardowo powinno znaleźć się również w deklaracji destruktora.

Stare - throw()

W poprzedniej wersji standardu istniało słowo kluczowe throw(), które obecnie jest uznawane za przestarzałe. Można było za jego pomocą wyspecyfikować jakiego rodzaju wyjątki mogą być wyrzucone z funkcji, albo (odpowiednik noexpect) że funkcja nie rzuci żadnego wyjątku.
int fun(std::string& b) throw();

17 listopada 2013

[C++11] Przeciążenia operatorów

Zbiór notatek które zebrałem studiując tematykę operatorów po lekturze "C++ Primer". Najczęściej przeciążanym (overload) operatorem, jest chyba operator nawiasowy () który stosuje się w celu tworzenia funktorów. C++11 sprawi, że w jego miejsce częściej będzie pojawiać się lambda. W wielu przypadkach nadal, warto korzystać z obiektów funkcyjnych dostarczonych wraz z biblioteką standardową (pozytywnie wypowiadał się na ten temat także Stephan T. Lavavej na GoingNative 2013 - Don’t Help the Compiler). Nie tylko potrafią być krótsze od lambd, ale potrafią też poprawnie pracować ze wskaźnikami.

Kiedy tego nie robić?

Przeciążenie (overload) operatora ma tylko sens, jeżeli programista nie będzie zaskoczony jego działaniem.
Standard nie gwarantuje w jakiej kolejności będą wołane operandy. Np. dla f() + g(), nie wiemy, która z funkcji zostanie zawołana jako pierwsza. Mamy za to pewność że priorytety operatorów będą zachowane (czyli np. mnożenie będzie przed dodawaniem) [C++Prime, s. 138].
Nie należy przeciążać operatora kropki (.), pobrania adresu (&), logicznego AND (&&) oraz logicznego OR (||).
#include <iostream>

using namespace std;

struct Num {
    Num(int v) : value(v) { }
    Num() : Num(0) { }

    int value;
};

Num operator+(const Num& first, const Num& second) {
    return Num(first.value + second.value);
}

Num operator*(const Num& first, const Num& second) {
    return Num(first.value * second.value);
}

int main()
{
    Num a(2);
    Num b(2);
    Num c(2);

    cout << (a + b).value << endl;
    cout << (a * b).value << endl;
    cout << (a * b + c).value << endl;
    cout << (a + b * c).value << endl;
}
Wynik:
4
4
6
6

Gdzie tworzyć deklaracje?

Kiedy już decydujemy się na przeciążenie operatora, stajemy przed dylematem, czy stworzyć go jako element składowy klasy, czy jako funkcję poza klasą. Pomocne mogą być poniższe wskazówki:
  • operatora przypisania (=), nawisu kwadratowego ([]), zawołania (()) oraz strzałki (->) muszą być zdefiniowane jako składniki klasy
  • operatory mieszane z przypisaniem (+=, -=, *=, /= itp.) generalnie powinny być składowymi klasy, ale w przeciwieństwie do samego operatora przypisania (=) nie jest to wymagane
  • operatory które zmieniają stan obiektu, albo są z nim ściśle powiązane np. inkrementacja (++), dekrementacja (--), dereferencja (*) zazwyczaj powinny być składowymi klasy
  • operatory symetryczne (takie które mogą konwertować którykolwiek z operandów) tj. arytmetyczne (+, -), równości (==), relacji (<, >, <=, <=) i bitowe (|, &, ^, <<, >>), zazwyczaj powinny być definiowane jako funkcje poza klasą

Szczegóły dla konkretnych operatorów

Operatory I/O - wejścia, wyjścia - (<<, >>), powinny być funkcjami poza klasą i powinny drukować zawartość z bardzo minimalnym formatowaniem. Jako, że czasami muszą mieć dostęp do niepublicznych danych, powinny być zadeklarowane jako przyjaciele (friend) klasy. Operator wejściowy powinien radzić sobie z sytuacją, gdy "wejście" zawiedzie, operator wyjściowy generalnie się tym nie przejmuje.

Dla operatora nawiasu kwadratowego ([]) warto zrobić wersję const oraz non-const

Dla operatorów inkrementacji (++) i dekrementacji (--) istnieją dwie wersje: prefiksowa i postfiksowa. Wersja postfiksowa dla rozróżnienia bierze jako argument dodatkowy nieużywany parametr typu int, kompilator wstawia tam zero.
#include <iostream>
using namespace std;

struct Counter {
    double d;
    Counter() : d(0.12) { }
    Counter operator++() {
        d += 1.0;
        cout << "prefix:  ++obj" << endl;
        return *this;
    }

    Counter operator++(int) {
        Counter tmp = *this;
        d += 1.0;
        cout << "postfix: obj++" << endl;
        return tmp;
    }
};

int main() {
    Counter c;
    c++;
    ++c;
}
Wynik:
postfix: obj++
prefix:  ++obj

Konwersje typów

Konwersje z jednego typu na drugi mogą być bardzo mylące. Czasami lepiej stworzyć odpowiednią metodą, która lepiej poinformuje nas o intencji danej konwersji. Operatory konwersji powinny być składowymi klasy, nie powinny określić typu zwracanego i posiadać pustą listą parametrów. Funkcja zazwyczaj powinna być const-owa.

We wcześniejszej wersji języka konwersja na typ bool była problematyczna, ponieważ bool jest typem arytmetycznym. Obiekt klasy która pozwala na konwersję na bool, mógł zostać użyty we wszystkich miejscach, gdzie wymagany jest typ arytmetyczny. Gdyby std::cin dało się konwertować na typ bool, możliwa była by operacja "std::cin << 34" - w rezultacie doszło by do przesunięcia bitowego (bool zostałby jeszcze skonwertowany na int).

Nowy standard wprowadza konwersje explicit. Powinna się ona ograniczyć raczej tylko do bool i zazwyczaj z intencją sprawdzania wyniku w jakimś warunku.
Jeżeli konwersja jest explicit, możemy jej używać ale tylko z rzutowaniem. Wyjątkiem jest niejawne (implicit) korzystanie z konwersji dla:
  • warunków w if, while oraz do-while
  • warunków w for
  • operatorów dla logicznego NOT (!), OR (||) oraz AND (&&)
  • warunków dla ?:
#include <iostream>
using namespace std;

struct SmallInt {
    double val;
    SmallInt(double v) : val(v) { }

    explicit operator int() {
        return val;
    }

    explicit operator bool() {
        return val > 3.0;
    }
};

int main() {
    SmallInt si(3.14);
//  cout << si + 1 << endl; // ERROR wymagana jest niejawna konwersja,
                            // ale operator rzutowania na int jest explicit
    cout << static_cast<int>(si) + 1 << endl;

    if (si)
        cout << "Bigger than PI" << endl;

    return 0;
}
Wynik:
4
Bigger than PI

28 października 2013

Going Native 2013

Zakończyłem oglądanie udostępnionych wykładów z GoingNative 2013. Pojawiło się na niej kilka slajdów z nowościami z C++14, ale żadne wykład nie był mu w całości zadedykowany. Z najfajniejszych (moim zdaniem) wykładów warto wymienić.
  • Writing Quick Code in C++, Quickly - sztuczki z optymalizacją kodu w bibliotece folly rozwijanej przez Facebook-a. Mam nadzieje, że nie będę musiał się nigdy kłopotać takimi szczegółami, nie mówiąc o przykładach, które nie do końca rozumiem. Kilka wniosków z prezentacji
    • Inżynierowie którzy znają architekturę swojego sprzętu piszą lepszy kod
    • "Mierz wszystko!". Aby sprawdzić wydajność programu wynikowego, trzeba badać go z każdej strony: czas wykonani, zużycie pamięci, zużycie energii, ...
    • Jeżeli dobrze zrozumiałem, czas kompilacji kodu może zostać skrócony, przez stosowanie final tam gdzie to możliwe. Niestety gcc i clang nadal nad tym pracują.
  • Don’t Help the Compiler - jak dla mnie tytuł troszkę mylący bo wykład opowiada o najczęstszych błędach jakie popełniają programiści. Garść notatek z prezentacji:
    • Jeżeli zwracamy wynik przez wartość, stosowanie const może wyłączyć move semantic. Nie korzystać z const dla wartości.
    • Wartości zwracane przez rvalue lub rvalue reference np. string& i string&& są tym samym. Czyli referencją do wartości.
    • Nie korzystać z move(), dla obiektów lokalnych które chcemy zwrócić. Jeżeli dobrze rozumiem, gdy zwracamy lokalną zmienną mogą zadziać się dwie rzeczy. Albo kompilator przeniesie (zoptymalizuje) konstrukcje tego obiektu do kodu wywołującego, albo zadziała move semantic (nie trzeba mu pomagać używając move).
    • Czasami korzystanie ze standardowych funktorów z STL-a tworzy bardziej elegancki kod, niż lambda np. std::plus<> itp.
  • An Effective C++11/14 Sampler - dużo ciekawych informacji na temat wątków oraz std::move i std::forward - sam jeszcze eksperymentuje z tym więc będzie osobny wpis.
    • Jeżeli chcemy szybkości (w postaci move semantic), przekazując przez wartość należy zrezygnować z const. To samo dotyczy zwracania. const wymusza kopiowanie!
    • Warto stosować noexcept, gdy to tylko możliwe generowany jest wtedy bardziej zoptymalizowany kod.
  • The Care and Feeding of C++’s Dragons - bardzo ciekawy wykład na temat rozszerzeń do clang-a mających na celu ułatwienie pracy programistom. Poeksperymentuje jak tylko uda się przekompilować clang-a.
  • rand() Considered Harmful - przestroga przed stosowaniem rand() z biblioteki C. Do C++11 trafiła biblioteka służąca do generowanie wartości losowych, bardzo podobna do Boost.Random.
  • C++ Seasoning - jak znajomość algorytmów standardowych (STL), może usprawnić refactoring kodu.
  • Inheritance Is The Base Class of Evil - trudne do zakumania, ale przykład działa na wyobraźnie. Może będzie kiedyś czas, by nad tym spokojnie pokontemplować.