Witamy z powrotem w języku C++ — Modern C++

Od czasu jego utworzenia język C++ stał się jednym z najczęściej używanych języków programowania na świecie. Dobrze napisane programy języka C++ są szybkie i wydajne. Język jest bardziej elastyczny niż inne języki: pozwala pracować zarówno na najwyższych poziomach abstrakcji, jak i na poziomie krzemu.

Język C++ dostarcza wysoce zoptymalizowane biblioteki standardowe. Umożliwia ona dostęp do funkcji sprzętowych niskiego poziomu, aby zmaksymalizować szybkość i zminimalizować wymagania dotyczące pamięci. W języku C++ można tworzyć niemal każdy rodzaj programów: gry, sterowniki urządzeń, aplikacje do obliczeń wysokowydajnych, aplikacje chmurowe, desktopowe, wbudowane i mobilne oraz wiele innych. Nawet biblioteki i kompilatory dla innych języków programowania są napisane w języku C++.

Jednym z oryginalnych wymagań dla języka C++ było zgodność z poprzednimi wersjami języka C. W rezultacie język C++ umożliwia programowanie w stylu C z nieprzetworzonymi wskaźnikami, tablicami, ciągami znaków zakończonymi wartościami null i innymi funkcjami. Mogą one zapewnić doskonałą wydajność, ale mogą również powodować błędy i złożoność.

Ewolucja języka C++ podkreśla funkcje, które znacznie zmniejszają potrzebę używania idiomów w stylu C. Stare mechanizmy programowania w C są nadal dostępne, kiedy ich potrzebujesz. Jednak w nowoczesnym kodzie C++ należy ich potrzebować mniej i mniej. Nowoczesny kod C++ jest prostszy, bezpieczniejszy, bardziej elegancki i wciąż tak szybki, jak zawsze.

Poniższe sekcje zawierają omówienie głównych funkcji nowoczesnego języka C++. O ile nie określono inaczej, wymienione tutaj funkcje są dostępne w języku C++11 lub nowszym. W kompilatorze Microsoft C++ można ustawić /std opcję kompilatora, aby określić, która wersja standardu ma być używana dla projektu.

Zasoby i inteligentne wskaźniki

Jedną z głównych klas błędów w programowaniu w stylu C jest wyciek pamięci. Wycieki pamięci są często spowodowane niewywołaniem funkcji delete dla pamięci przydzielonej za pomocą new. Nowoczesny C++ podkreśla zasadę pozyskiwanie zasobów to inicjalizacja (RAII).

Pomysł jest prosty. Zasoby, takie jak pamięć sterty, uchwyty plików i gniazda, powinny być własnością obiektu. Ten obiekt tworzy lub otrzymuje nowo zaalokowany zasób w swoim konstruktorze, a usuwa go w swoim destruktorze. Zasada RAII gwarantuje, że wszystkie zasoby są prawidłowo zwracane do systemu operacyjnego, gdy obiekt będący ich właścicielem wychodzi poza zakres.

Aby ułatwić wdrażanie zasad RAII, biblioteka Standardowa języka C++ udostępnia trzy inteligentne typy wskaźników:

Inteligentny wskaźnik zarządza alokacją i zwalnianiem pamięci, której jest właścicielem. W poniższym przykładzie pokazano klasę z składową tablicy przydzieloną na stercie w wywołaniu metody make_unique(). Klasa unique_ptr enkapsuluje wywołania do new i delete. Gdy obiekt widget wychodzi poza zakres, destruktor unique_ptr zostanie wywołany i zwolni pamięć przydzieloną dla tej tablicy.

#include <memory>
class widget
{
private:
    std::unique_ptr<int[]> data;
public:
    widget(const int size) { data = std::make_unique<int[]>(size); }
    void do_something() {}
};

void functionUsingWidget() {
    widget w(1000000);  // lifetime automatically tied to enclosing scope
                        // constructs w, including the w.data gadget member
    // ...
    w.do_something();
    // ...
} // automatic destruction and deallocation for w and w.data

Jeśli to możliwe, używaj inteligentnego wskaźnika do zarządzania pamięcią na stercie. Jeśli musisz jawnie używać operatorów new i delete, przestrzegaj zasady RAII. Aby uzyskać więcej informacji, zobacz Okres istnienia obiektów i zarządzanie zasobami (RAII).

std::string i std::string_view

Ciągi w stylu C są kolejnym głównym źródłem usterek. Za pomocą poleceń std::string i std::wstringmożna wyeliminować praktycznie wszystkie błędy skojarzone z ciągami w stylu C. Dodatkowo zyskujesz możliwość korzystania z funkcji składowych do wyszukiwania, dołączania na końcu, dołączania na początku itd. Oba są wysoce zoptymalizowane pod kątem szybkości. Podczas przekazywania ciągu do funkcji, która wymaga tylko dostępu tylko do odczytu, w języku C++17 można użyć std::string_view w celu uzyskania jeszcze większej korzyści wydajności.

std::vector i inne kontenery biblioteki standardowej

Standardowe kontenery biblioteki są zgodne z zasadą RAII. Zapewniają iteratory do bezpiecznego iterowania po elementach. Są one wysoce zoptymalizowane pod kątem wydajności i dokładnie przetestowane pod kątem poprawności. Korzystając z tych kontenerów, można wyeliminować potencjał błędów lub nieefektywności, które mogą zostać wprowadzone w niestandardowych strukturach danych. Zamiast nieprzetworzonych tablic użyj vector jako kontenera sekwencyjnego w języku C++.

vector<string> apples;
apples.push_back("Granny Smith");

Użyj map (nie unordered_map) jako domyślnego kontenera asocjacyjnego. Użyj set, multimap i multiset dla przypadków zdegenerowanych i wielokrotnych.

map<string, string> apple_color;
// ...
apple_color["Granny Smith"] = "Green";

Jeśli potrzebujesz optymalizacji wydajności, rozważ użycie:

  • Nieurządkowane kontenery asocjacyjne, takie jak unordered_map. Te kontenery mają mniejszy narzut na element i umożliwiają wyszukiwanie w czasie stałym, ale mogą być trudniejsze w poprawnym i wydajnym użyciu.
  • Posortowane vector. Aby uzyskać więcej informacji, zobacz Algorytmy.

Nie używaj tablic w stylu C. W przypadku starszych interfejsów API, które wymagają bezpośredniego dostępu do danych, należy zamiast tego użyć metod dostępowych, takich jak f(vec.data(), vec.size());. Aby uzyskać więcej informacji na temat kontenerów, zobacz C++ Standard Library Containers (Kontenery biblioteki standardowej języka C++).

Algorytmy biblioteki standardowej

Przed założeniem, że musisz napisać niestandardowy algorytm dla programu, zapoznaj się z algorytmami biblioteki standardowej języka C++. Biblioteka Standardowa zawiera coraz większy asortyment algorytmów dla wielu typowych operacji, takich jak wyszukiwanie, sortowanie, filtrowanie i losowanie. Biblioteka matematyczna jest obszerna. W języku C++17 lub nowszym udostępniane są równoległe wersje wielu algorytmów.

Oto kilka ważnych przykładów:

  • for_each: domyślny algorytm iteracji wraz z pętlami for opartymi na zakresach.
  • transform: w przypadku modyfikacji elementów kontenera w miejscu.
  • find_if: domyślny algorytm wyszukiwania.
  • sort, lower_boundi inne domyślne algorytmy sortowania i wyszukiwania.

Aby napisać komparator, używaj ścisłych < i nazwanych wyrażeń lambda, gdy to możliwe.

auto comp = [](const widget& w1, const widget& w2)
     { return w1.weight() < w2.weight(); }

sort( v.begin(), v.end(), comp );

auto i = lower_bound( v.begin(), v.end(), widget{0}, comp );

auto zamiast jawnych nazw typów

Język C++11 wprowadził auto słowo kluczowe do użycia w deklaracjach zmiennych, funkcji i szablonów. auto polecenie kompilatorowi, aby wyłudić typ obiektu, aby nie trzeba było wpisać go jawnie. auto jest szczególnie przydatne, gdy wywnioskowany typ jest zagnieżdżonym szablonem:

map<int,list<string>>::iterator i = m.begin(); // C-style
auto i = m.begin(); // modern C++

Pętle for zakresowe

Iteracja w stylu C dla tablic i kontenerów jest podatna na błędy indeksowania i jest również żmudna do wpisywania. Aby wyeliminować te błędy i zwiększyć czytelność swojego kodu, użyj pętli for opartych na zakresie zarówno w przypadku kontenerów Biblioteki standardowej, jak i zwykłych tablic. Aby uzyskać więcej informacji, zobacz instrukcję for opartą na zakresie.

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v {1,2,3};

    // C-style
    for(int i = 0; i < v.size(); ++i)
    {
        std::cout << v[i];
    }

    // Modern C++:
    for(auto& num : v)
    {
        std::cout << num;
    }
}

constexpr wyrażenia zamiast makr

Makra w języku C i C++ to tokeny przetwarzane przez preprocesor przed kompilacją. Każde wystąpienie tokenu makra jest zastępowane zdefiniowaną wartością lub wyrażeniem przed skompilowanym plikiem. Makra są często używane w programowaniu w stylu C do definiowania wartości stałych w czasie kompilacji. Jednak makra są podatne na błędy i trudne do debugowania. W nowoczesnym języku C++ należy preferować zmienne constexpr jako stałe znane w czasie kompilacji:

#define SIZE 10 // C-style
constexpr int size = 10; // modern C++

Jednolite inicjowanie

W nowoczesnym języku C++można użyć inicjowania nawiasu klamrowego dla dowolnego typu. Ta forma inicjowania jest szczególnie wygodna podczas inicjowania tablic, wektorów lub innych kontenerów. W poniższym przykładzie v2 jest inicjowany trzema instancjami S. v3 jest inicjowany trzema instancjami S, które same są inicjowane przy użyciu nawiasów klamrowych. Kompilator wywnioskuje typ każdego elementu na podstawie zadeklarowanego typu v3.

#include <vector>

struct S
{
    std::string name;
    float num;
    S(std::string s, float f) : name(s), num(f) {}
};

int main()
{
    // C-style initialization
    std::vector<S> v;
    S s1("Norah", 2.7);
    S s2("Frank", 3.5);
    S s3("Jeri", 85.9);

    v.push_back(s1);
    v.push_back(s2);
    v.push_back(s3);

    // Modern C++:
    std::vector<S> v2 {s1, s2, s3};

    // or...
    std::vector<S> v3{ {"Norah", 2.7}, {"Frank", 3.5}, {"Jeri", 85.9} };

}

Aby uzyskać więcej informacji, zobacz Inicjowanie nawiasu klamrowego.

Przenoszenie semantyki

Nowoczesny język C++ oferuje semantykę przenoszenia, która pozwala wyeliminować niepotrzebne kopie pamięci. We wcześniejszych wersjach języka kopie były nieuniknione w niektórych sytuacjach. Operacja przenoszenia przenosi własność zasobu z jednego obiektu do następnego bez tworzenia kopii. Niektóre klasy zarządzają zasobami, takimi jak pamięć sterty, uchwyty plików i tak dalej.

Implementując klasę zarządzającą zasobami, można zdefiniować dla niej konstruktor przenoszący i operator przypisania przenoszącego. Kompilator wybiera te specjalne elementy członkowskie podczas rozwiązywania przeciążeń w sytuacjach, gdy kopia nie jest potrzebna. Typy kontenerów biblioteki standardowej wywołują konstruktor przenoszenia dla obiektów, jeśli jest zdefiniowany. Aby uzyskać więcej informacji, zobacz Konstruktory przenoszące i operatory przypisania przenoszącego (C++).

Wyrażenia lambda

W programowaniu w stylu C funkcja może być przekazywana do innej funkcji przy użyciu wskaźnika funkcji. Wskaźniki do funkcji są trudne w utrzymaniu i zrozumieniu. Funkcja, do której się odwołuje, może być zdefiniowana gdzie indziej w kodzie źródłowym, daleko od punktu, w którym jest wywoływana. Ponadto nie są one bezpieczne.

Nowoczesny język C++ udostępnia obiekty funkcji, klasy, które zastępują operator() operator, co umożliwia ich wywoływanie jak funkcja. Najwygodniejszym sposobem tworzenia obiektów funkcji jest użycie wbudowanych wyrażeń lambda. W poniższym przykładzie pokazano, jak za pomocą wyrażenia lambda przekazać obiekt funkcji wywoływany find_if przez funkcję dla każdego elementu w wektorze:

    std::vector<int> v {1,2,3,4,5};
    int x = 2;
    int y = 4;
    auto result = find_if(begin(v), end(v), [=](int i) { return i > x && i < y; });

Wyrażenie [=](int i) { return i > x && i < y; } lambda może być odczytywane jako "funkcja, która przyjmuje pojedynczy argument typu int i zwraca wartość logiczną wskazującą, czy argument jest większy niż x i mniejszy niż y." Zwróć uwagę, że zmienne x i y z otaczającego kontekstu mogą być używane w lambda. Określa [=] , że te zmienne są przechwytywane przez wartość. Innymi słowy wyrażenie lambda ma własne kopie tych wartości.

Wyjątki

Nowoczesny język C++ podkreśla wyjątki, a nie kody błędów, jako najlepszy sposób raportowania i obsługi warunków błędów. Aby uzyskać więcej informacji, zobacz Modern C++ best practices for exceptions and error handling (Nowoczesne rozwiązania w języku C++ dotyczące wyjątków i obsługi błędów).

std::atomic

Użyj struktury std::atomic i powiązanych z nią typów ze standardowej biblioteki C++ do komunikacji między wątkami.

std::variant (C++17)

Unie są powszechnie używane w programowaniu w stylu języka C do oszczędzania pamięci, ponieważ pozwalają składowym różnych typów zajmować ten sam obszar pamięci. Unie nie są bezpieczne typowo i są podatne na błędy w programowaniu. Język C++17 wprowadza klasę std::variant jako bardziej niezawodną i bezpieczną alternatywę dla związków zawodowych. Funkcja std::visit może służyć do uzyskiwania dostępu do składowych variant typu w bezpieczny sposób.

Zobacz też