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: może działać na najwyższym poziomie abstrakcji i w dół 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. Język C++ może tworzyć niemal dowolny rodzaj programu: gry, sterowniki urządzeń, HPC, chmura, komputery stacjonarne, osadzone i mobilne oraz wiele innych. Nawet biblioteki i kompilatory dla innych języków programowania są zapisywane w języku C++.

Jednym z oryginalnych wymagań dla języka C++ było zgodność z poprzednimi wersjami języka C. W związku z tym język C++ zawsze zezwalał na 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śliła funkcje, które znacznie zmniejszają potrzebę używania idiomów w stylu C. Stare obiekty programowania C są nadal tam, gdy 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. Przecieki są często spowodowane przez błąd wywołania delete pamięci przydzielonej za pomocą newpolecenia . Modern C++ podkreśla zasadę pozyskiwania zasobów jest inicjowanie (RAII). Pomysł jest prosty. Zasoby (pamięć sterty, dojścia plików, gniazda itd.) powinny być własnością obiektu. Ten obiekt tworzy lub odbiera nowo przydzielony zasób w konstruktorze i usuwa go w destruktorze. Zasada RAII gwarantuje, że wszystkie zasoby są prawidłowo zwracane do systemu operacyjnego, gdy obiekt będąc właścicielem wykracza poza zakres.

Aby ułatwić wdrażanie zasad RAII, biblioteka Standardowa języka C++ udostępnia trzy inteligentne typy wskaźników: std::unique_ptr, std::shared_ptri std::weak_ptr. Inteligentny wskaźnik obsługuje alokację i usunięcie pamięci, która jest jej właścicielem. W poniższym przykładzie pokazano klasę z składową tablicy przydzieloną na stercie w wywołaniu metody make_unique(). Wywołania klasy new i delete są hermetyzowane przez klasę unique_ptr . widget Gdy obiekt wykracza poza zakres, zostanie wywołany destruktor unique_ptr i zwolni pamięć przydzieloną dla 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żyj inteligentnego wskaźnika do zarządzania pamięcią stert. Jeśli musisz jawnie użyć new operatorów i delete , postępuj zgodnie z zasadą 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. Zyskujesz również korzyść z funkcji składowych na potrzeby wyszukiwania, dołączania, dołączania, dołączania i tak dalej. 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 przechodzenia elementów. Są one wysoce zoptymalizowane pod kątem wydajności i zostały 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 wartości (nie unordered_map) jako domyślnego kontenera asocjacji. Użyj setwartości , multimapi multiset w przypadku degeneracji i wielu przypadków.

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

W razie potrzeby optymalizacji wydajności rozważ użycie:

  • Nieurządkowane kontenery asocjacyjne, takie jak unordered_map. Mają one mniejsze obciążenie poszczególnych elementów i wyszukiwanie w stałym czasie, ale mogą być trudniejsze do prawidłowego i wydajnego użycia.
  • 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 użyć metod dostępu, 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, najpierw zapoznaj się z algorytmami standardowej biblioteki 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 przechodzenia (wraz z pętlami opartymi na for zakresie).
  • transform, dla nie-w miejscu modyfikacji elementów kontenera
  • find_if, domyślny algorytm wyszukiwania.
  • sort, lower_boundi inne domyślne algorytmy sortowania i wyszukiwania.

Aby napisać komparator, należy używać surowych < i używać nazwanych lambdów , 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 typ wywołany jest szablonem zagnieżdżonym:

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

Pętle oparte na for zakresie

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ć czytelny kod, użyj pętli opartych na for zakresie zarówno z kontenerami biblioteki standardowej, jak i nieprzetworzonymi tablicami. Aby uzyskać więcej informacji, zobacz Instrukcja oparta na for 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ć constexpr zmienne dla stałych czasu 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 zainicjowano trzy wystąpienia klasy S. v3 jest inicjowany z trzema S wystąpieniami, 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++ zapewnia semantyka przenoszenia, co umożliwia wyeliminowanie niepotrzebnych kopii 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 posiadają zasoby, takie jak pamięć sterta, dojścia plików itd. Implementując klasę będącą właścicielem zasobów, można zdefiniować dla niej konstruktor przenoszenia i operator przypisania przenoszenia. 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 Przenoszenie konstruktorów i operatorów przypisania przenoszenia (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 funkcji są niewygodne, aby zachować i zrozumieć. 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, który find_if będzie wywoływany 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 standardowej biblioteki std::atomic języka C++ i powiązanych typów dla mechanizmów komunikacji międzywątkowa.

std::variant (C++17)

Związki są często używane w programowaniu w stylu C do oszczędzania pamięci przez umożliwienie członkom różnych typów zajmowania tej samej lokalizacji pamięci. Jednak związki nie są bezpieczne i podatne na błędy programowania. 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ż

Dokumentacja języka C++
Wyrażenia lambda
Standardowa biblioteka C++
Zgodność języka Microsoft C/C++