Obsługa ciągów C++ - C++ string handling

Język programowania C++ obsługuje obsługę ciągów znaków , głównie zaimplementowaną w jego standardowej bibliotece . Standard językowy określa kilka typów łańcuchów, niektóre odziedziczone z C , niektóre zaprojektowane do korzystania z cech języka, takich jak klasy i RAII . Najczęściej używanym z nich jest std::string .

Ponieważ początkowe wersje C++ miały tylko "niskopoziomowe" funkcje obsługi ciągów C i konwencje, wiele niekompatybilnych projektów klas obsługi ciągów zostało zaprojektowanych przez lata i nadal są używane zamiast std::string, a programiści C++ mogą potrzebować obsługiwać wiele konwencji w jednej aplikacji.

Historia

Typ std::string jest głównym łańcuchowym typem danych w standardowym C++ od 1998 roku, ale nie zawsze był częścią C++. Od C, C++ odziedziczył konwencję używania ciągów zakończonych znakiem null, które są obsługiwane przez wskaźnik do ich pierwszego elementu, oraz bibliotekę funkcji, które manipulują takimi ciągami. We współczesnym standardzie C++ literał ciągu, taki jak „hello”, nadal oznacza tablicę znaków zakończoną znakiem NUL.

Użycie klas C++ do zaimplementowania typu ciągu oferuje kilka korzyści związanych z automatycznym zarządzaniem pamięcią i zmniejszeniem ryzyka dostępu poza granice oraz bardziej intuicyjną składnię do porównywania i łączenia ciągów. Dlatego bardzo kuszące było stworzenie takiej klasy. Przez lata twórcy aplikacji, bibliotek i frameworków w języku C++ stworzyli własne, niezgodne reprezentacje łańcuchowe, takie jak ta w bibliotece Standard Components AT&T (pierwsza taka implementacja, 1983) lub typ CString w MFC firmy Microsoft . Podczas gdy std::string standaryzowane ciągi, starsze aplikacje nadal często zawierają takie niestandardowe typy ciągów i biblioteki mogą oczekiwać ciągów w stylu C, co czyni „praktycznie niemożliwym” uniknięcie używania wielu typów ciągów w programach C++ i wymaga od programistów decydowania o pożądanym ciągu reprezentacja przed rozpoczęciem projektu.

W 1991 roku w retrospekcji na temat historii C++, jego wynalazca Bjarne Stroustrup nazwał brak standardowego typu string (i niektórych innych standardowych typów) w C++ 1.0 najgorszym błędem, jaki popełnił podczas jego rozwoju; „Brak tych doprowadził do tego, że wszyscy wynaleźli koło na nowo i do niepotrzebnej różnorodności w najbardziej podstawowych klasach”.

Problemy wdrożeniowe

Różne typy ciągów dostawców mają różne strategie implementacji i charakterystyki wydajności. W szczególności niektóre typy ciągów używają strategii kopiowania przy zapisie , w której operacja taka jak

string a = "hello!";
string b = a; // Copy constructor

faktycznie nie kopiuje zawartości a do b ; zamiast tego oba ciągi współdzielą swoją zawartość, a liczba odwołań do zawartości jest zwiększana. Rzeczywiste kopiowanie jest odkładane do czasu, gdy operacja mutacji, taka jak dołączenie znaku do dowolnego ciągu, spowoduje, że zawartość ciągów będzie się różnić. Kopiowanie przy zapisie może spowodować znaczne zmiany wydajności kodu za pomocą ciągów znaków (co powoduje, że niektóre operacje są znacznie szybsze, a inne znacznie wolniejsze). Chociaż std::string już go nie używa, wiele (być może większość) alternatywnych bibliotek ciągów nadal implementuje ciągi kopiowania przy zapisie.

Niektóre implementacje ciągów przechowują 16-bitowe lub 32-bitowe punkty kodowe zamiast bajtów, co miało na celu ułatwienie przetwarzania tekstu Unicode . Oznacza to jednak, że konwersja do tych typów z std::string lub z tablic bajtów jest powolną i często stratną operacją, zależną od „locale” i może zgłaszać wyjątki. Wszelkie zalety przetwarzania 16-bitowych jednostek kodu zniknęły, gdy wprowadzono kodowanie UTF-16 o zmiennej szerokości (chociaż nadal istnieją korzyści, jeśli musisz komunikować się z 16-bitowym interfejsem API, takim jak Windows). Qt jest QString przykład.

Implementacje ciągów innych firm również znacznie różniły się składnią do wyodrębniania lub porównywania podciągów lub przeprowadzania wyszukiwania w tekście.

Standardowe typy strun

Klasa std::string jest standardową reprezentacją ciągu tekstowego od C++98 . Klasa udostępnia kilka typowych operacji na ciągach, takich jak porównywanie, łączenie, znajdowanie i zastępowanie oraz funkcję do uzyskiwania podciągów . Std :: string może być zbudowany z łańcucha w stylu C, a łańcuch w stylu C można również otrzymać od jednego.

Poszczególne jednostki tworzące łańcuch są typu char , co najmniej (i prawie zawsze) 8 bitów każda. We współczesnym użyciu często nie są to „znaki”, ale części wielobajtowego kodowania znaków, takiego jak UTF-8 .

Strategia kopiowania przy zapisie została celowo dozwolona przez początkowy standard C++ dla std::string, ponieważ uznano ją za użyteczną optymalizację i była używana przez prawie wszystkie implementacje. Zdarzały się jednak błędy, w szczególności operator[] zwracał referencję niestałą, aby ułatwić manipulacje ciągami znaków w miejscu C (taki kod często zakładał jeden bajt na znak i dlatego może to nie być dobre pomysł!) Umożliwiło to następujący kod, który pokazuje, że musi wykonać kopię, mimo że prawie zawsze jest używany tylko do sprawdzenia ciągu, a nie do jego modyfikacji:

  std::string original("aaaaaaa");
  std::string string_copy = original; // make a copy
  char* pointer = &string_copy[3]; // some tried to make operator[] return a "trick" class but this makes it complex
  arbitrary_code_here(); // no optimizations can fix this
  *pointer = 'b'; // if operator[] did not copy, this would change original unexpectedly

To spowodowało, że niektóre implementacje zrezygnowały z kopiowania przy zapisie. Odkryto również, że obciążenie w aplikacjach wielowątkowych ze względu na blokowanie potrzebne do zbadania lub zmiany liczby odwołań było większe niż obciążenie związane z kopiowaniem małych ciągów na nowoczesnych procesorach (zwłaszcza w przypadku ciągów mniejszych niż rozmiar wskaźnika). Optymalizacja została ostatecznie zabroniona w C++11 , w wyniku czego nawet przekazanie std::string jako argumentu do funkcji, mianowicie.

void print(std::string s) { std::cout << s; }

należy oczekiwać, że wykona pełną kopię ciągu do nowo przydzielonej pamięci. Powszechnym idiomem pozwalającym uniknąć takiego kopiowania jest przekazywanie jako stałego odniesienia :

void print(const std::string& s) { std::cout << s; }

W C++17 dodano nową klasę string_view, która jest tylko wskaźnikiem i długością do danych tylko do odczytu, dzięki czemu przekazywanie argumentów jest znacznie szybsze niż którykolwiek z powyższych przykładów:

void print(std::string_view s) { std::cout << s; }
...
  std::string x = ...;
  print(x); // does not copy x.data()
  print("this is a literal string"); // also does not copy the characters!
...


Przykładowe użycie

#include <iostream>
#include <iomanip>
#include <string>

int main() {
    std::string foo = "fighters";
    std::string bar = "stool";
    if (foo != bar) std::cout << "The strings are different!\n";
    std::cout << "foo = " << std::quoted(foo)
              << " while bar = " << std::quoted(bar);
}

Powiązane zajęcia

std::string jest typem definicji dla określonej instancji klasy szablonu std::basic_string . Jego definicja znajduje się w nagłówku <string> :

using string = std::basic_string<char>;

W ten sposób string zapewnia funkcjonalność basic_string dla ciągów zawierających elementy typu char . Istnieje podobna klasa std::wstring , która składa się z wchar_t i jest najczęściej używana do przechowywania tekstu UTF-16 w systemie Windows i UTF-32 na większości platform uniksowych . Jednak standard C++ nie narzuca żadnej interpretacji jako punktów kodowych lub jednostek kodu Unicode dla tych typów i nie gwarantuje nawet, że wchar_t przechowuje więcej bitów niż char . Aby rozwiązać niektóre niezgodności wynikające z właściwości wchar_t , C++11 dodał dwie nowe klasy: std::u16string i std::u32string (składające się z nowych typów char16_t i char32_t ), które są określoną liczbą bitów na jednostkę kodu na wszystkich platformach. C++11 dodał także nowe literały ciągów 16-bitowych i 32-bitowych „znaków” oraz składnię do umieszczania punktów kodowych Unicode w ciągach zakończonych znakiem null (w stylu C).

Basic_string jest gwarancją specializable dla każdego typu z char_traits struct aby go towarzyszyć. Od C++11 w standardowej bibliotece muszą być zaimplementowane tylko specjalizacje char , wchar_t , char16_t i char32_t ; wszystkie inne typy są zdefiniowane w implementacji. Każda specjalizacja jest również kontenerem biblioteki standardowej , a zatem algorytmy biblioteki standardowej mogą być stosowane do jednostek kodu w ciągach.

Krytyka

Projekt std::string został uznany za przykład projektowania monolitycznego przez Herba Suttera , który uważa, że ​​ze 103 funkcji składowych w klasie w C++98, 71 mogło zostać odsprzężonych bez utraty wydajności implementacji.

Bibliografia