Kiedy powinienem używać string_view w interfejsie?

16

Korzystam z biblioteki wewnętrznej, która została zaprojektowana tak, aby naśladować proponowaną bibliotekę C ++ , a czasami w ciągu ostatnich kilku lat widzę, że jej interfejs zmienił się z używania std::stringna string_view.

Dlatego sumiennie zmieniam kod, aby dostosować się do nowego interfejsu. Niestety, muszę przekazać parametr std :: string i coś, co jest wartością zwracaną std :: string. Więc mój kod zmienił się z czegoś takiego:

void one_time_setup(const std::string & p1, int p2) {
   api_class api;
   api.setup (p1, special_number_to_string(p2));
}

do

void one_time_setup(const std::string & p1, int p2) {
   api_class api;
   const std::string p2_storage(special_number_to_string(p2));
   api.setup (string_view(&p1[0], p1.size()), string_view(&p2_storage[0], p2_storage.size()));
}

I naprawdę nie wiem, co ta zmiana kupił mnie jako klienta API, inne niż więcej kodu (aby ewentualnie zepsuć). Wywołanie API jest mniej bezpieczne (ponieważ API nie jest już właścicielem pamięci dla swoich parametrów), prawdopodobnie zapisał moją pracę programu 0 (ze względu na optymalizacje przenoszenia kompilatory mogą teraz zrobić), a nawet gdyby zaoszczędził pracę, byłoby to tylko kilka przydziałów, które nie zostaną i nigdy nie zostaną wykonane po starcie lub gdzieś w dużej pętli. Nie dla tego API.

Podejście to wydaje się jednak zgodne z radą, którą widzę gdzie indziej, na przykład z odpowiedzią :

Na marginesie, od C ++ 17 powinieneś unikać przekazywania const std :: string & na rzecz std :: string_view:

Uważam tę radę za zaskakującą, ponieważ wydaje się, że opowiada się ona za uniwersalnym zastąpieniem stosunkowo bezpiecznego obiektu mniej bezpiecznym (w zasadzie uwielbionym wskaźnikiem i długością), przede wszystkim w celach optymalizacji.

Kiedy więc należy użyć string_view, a kiedy nie?

PRZETRZĄSAĆ
źródło
1
nigdy nie powinieneś std::string_viewbezpośrednio wywoływać konstruktora, powinieneś po prostu przekazać łańcuchy do metody pobierającej std::string_viewbezpośrednio, a ona automatycznie się skonwertuje.
Mgetz
@Mgetz - Hmmm. Nie używam (jeszcze) w pełni funkcjonalnego kompilatora C ++ 17, więc być może to jest większość problemu. Mimo to przykładowy kod tutaj wydawał się wskazywać na jego wymagany, przynajmniej przy deklarowaniu jednego.
TED
4
Zobacz moją odpowiedź, operator konwersji znajduje się w <string>nagłówku i dzieje się to automatycznie. Ten kod jest zwodniczy i zły.
Mgetz
1
„z mniej bezpiecznym” w jaki sposób plasterek jest mniej bezpieczny niż odniesienie do ciągu?
CodesInChaos
3
@ TED Osoba dzwoniąca może równie łatwo uwolnić ciąg, na który wskazuje odniesienie, jak może zwolnić pamięć, na którą wskazuje wycinek.
CodesInChaos

Odpowiedzi:

18
  1. Czy funkcjonalność przyjmująca wartość musi przejąć na własność ciąg? Jeśli tak, użyj std::string(non-const, non-ref). Ta opcja daje również możliwość jawnego przejścia do wartości, jeśli wiesz, że nie będzie ona nigdy więcej używana w kontekście wywoływania.
  2. Czy funkcjonalność po prostu odczytuje ciąg? Jeśli tak, użyj std::string_view(const, non-ref), ponieważ string_viewmoże to obsługiwać std::stringi char*łatwo bez problemu i bez robienia kopii. To powinno zastąpić wszystkie const std::string&parametry.

Ostatecznie nigdy nie powinieneś dzwonić do std::string_viewkonstruktora takim, jakim jesteś. std::stringma operator konwersji, który automatycznie obsługuje konwersję.

Mgetz
źródło
Żeby wyjaśnić jeden punkt, myślę, że taki operator konwersji zająłby się także najgorszymi problemami w całym życiu, upewniając się, że wartość ciągu RHS pozostaje na całej długości połączenia?
TED,
3
@ TED, jeśli tylko odczytujesz wartość, wartość ta przetrwa połączenie. Jeśli przejmujesz własność, musi przetrwać połączenie. Stąd dlaczego rozwiązałem oba przypadki. Operator konwersji zajmuje się jedynie std::string_viewułatwieniem użytkowania. Jeśli programista niewłaściwie używa go w sytuacji, gdy jest właścicielem, jest to błąd programowy. std::string_viewjest całkowicie nieposiadający.
Mgetz
Dlaczego const, non-ref? Parametr będący const zależy od konkretnego zastosowania, ale ogólnie jest uzasadniony jako non-const. I spudłowałeś 3.
Potrafię
Na czym polega problem przejścia const std::string_view &w miejsce const std::string &?
ceztko
@ceztko jest całkowicie niepotrzebne i dodaje dodatkowej pośredniczości podczas uzyskiwania dostępu do danych.
Mgetz,
15

A std::string_viewprzynosi niektóre zalety A const char*do C ++: w przeciwieństwie std::stringdo string_view

  • nie ma pamięci,
  • nie przydziela pamięci,
  • może wskazywać na istniejący ciąg z pewnym przesunięciem, oraz
  • ma jeden poziom wskaźnika pośredniego niższy niż std::string&.

Oznacza to, że string_view może często unikać kopiowania, bez konieczności radzenia sobie z surowymi wskaźnikami.

W nowoczesnym kodzie std::string_viewpowinien zastąpić prawie wszystkie zastosowania const std::string&parametrów funkcji. Powinna to być zmiana zgodna ze źródłem, ponieważ std::stringdeklaruje operator konwersji na std::string_view.

To, że widok ciągu nie pomaga w konkretnym przypadku użycia, w którym musisz utworzyć ciąg, nie oznacza, że ​​jest to ogólnie zły pomysł. Standardowa biblioteka C ++ jest zazwyczaj zoptymalizowana pod kątem ogólności, a nie wygody. Argument „mniej bezpieczny” nie ma zastosowania, ponieważ samodzielne tworzenie widoku łańcucha nie powinno być konieczne.

amon
źródło
2
Dużą wadą std::string_viewjest brak c_str()metody, co powoduje, że niepotrzebne, pośrednie std::stringobiekty muszą zostać zbudowane i przydzielone. Jest to szczególnie problem w interfejsach API niskiego poziomu.
Matthias
1
@Matthias To dobra uwaga, ale nie sądzę, że jest to ogromna wada. Widok ciągu pozwala wskazać istniejący ciąg z pewnym przesunięciem. Podłańcuch nie może być zerowany, potrzebujesz do tego kopii. Widok ciągów nie zabrania tworzenia kopii. Umożliwia wiele zadań przetwarzania ciągów, które można wykonać za pomocą iteratorów. Ale masz rację, że interfejsy API wymagające ciągu C nie skorzystają na widokach. Odwołanie do ciągu może być wtedy bardziej odpowiednie.
amon
@Matthias, czy string_view :: data () nie pasuje do c_str ()?
Aelian
3
@Jeevaka ciąg C musi być zakończony zerem, ale dane widoku ciągu zwykle nie są zakończone zerem, ponieważ wskazuje na istniejący ciąg. Np. Jeśli mamy ciąg znaków abcdef\0i widok ciągów, który wskazuje na cdepodłańcuch, po znaku nie ma znaku zerowego e- oryginalny ciąg znaków ma ftam. W standardowych odnotowuje również dane (”) może powrócić wskaźnik bufor, który nie jest zerem zakończone. Dlatego zazwyczaj błędem jest przekazywanie danych () do funkcji, która przyjmuje tylko const charT * i oczekuje łańcucha zakończonego znakiem null. ”
amon
1
@kayleeFrye_onDeck Dane są już wskaźnikiem char. Problemem w łańcuchach C nie jest uzyskanie wskaźnika char, ale to, że łańcuch C musi być zakończony zerem. Zobacz mój poprzedni komentarz jako przykład.
amon
8

Uważam tę radę za zaskakującą, ponieważ wydaje się, że opowiada się ona za uniwersalnym zastąpieniem stosunkowo bezpiecznego obiektu mniej bezpiecznym (w zasadzie uwielbionym wskaźnikiem i długością), przede wszystkim w celach optymalizacji.

Myślę, że jest to nieco nieporozumienie w tym celu. Chociaż jest to „optymalizacja”, naprawdę powinieneś myśleć o tym jako o odrywaniu się od konieczności używania std::string.

Użytkownicy C ++ stworzyli dziesiątki różnych klas ciągów. Klasy ciągów o stałej długości, klasy zoptymalizowane pod kątem SSO z parametrem szablonu bufora, klasy ciągów przechowujące wartość skrótu używaną do ich porównywania itp. Niektóre osoby używają nawet ciągów opartych na COW. Jeśli jest coś, co programiści C ++ lubią robić, to pisz klasy ciągów.

I to ignoruje ciągi, które są tworzone i będące własnością bibliotek C. Nadzy char*, może o jakimś rozmiarze.

Więc jeśli piszesz jakąś bibliotekę, a bierzesz const std::string&, użytkownik musi teraz pobrać dowolny ciąg, którego używał i skopiować go do std::string. Może dziesiątki razy.

Jeśli chcesz uzyskać dostęp do std::stringinterfejsu specyficznego dla łańcucha, dlaczego musisz go skopiować ? To takie marnotrawstwo.

Główne powody, dla których nie należy przyjmować string_viewparametru jako:

  1. Jeśli Twoim ostatecznym celem jest przekazanie łańcucha do interfejsu, który pobiera łańcuch zakończony znakiem NUL ( fopenitp.). std::stringgwarantowane jest zakończenie NUL; string_viewnie jest. I bardzo łatwo jest podciąć widok, aby nie był zakończony NUL; std::stringpodciągi a skopiują podciąg do zakresu zakończonego NUL.

    Napisałem specjalny typ stylu string_view zakończony NUL dla dokładnie tego scenariusza. Możesz wykonywać większość operacji, ale nie takich, które niszczą status zakończony przez NUL (na przykład przycinanie od końca).

  2. Problemy z życiem. Jeśli naprawdę potrzebujesz skopiować tę std::stringtablicę lub w inny sposób mieć tablicę znaków, która przeżyje wywołanie funkcji, najlepiej jest to stwierdzić z góry, wykonując znak const std::string &. Lub po prostu std::stringjako parametr wartości. W ten sposób, jeśli mają już taki ciąg, możesz natychmiast przejąć jego własność, a osoba dzwoniąca może przenieść się do ciągu, jeśli nie musi przechowywać jego kopii.

Nicol Bolas
źródło
Czy to prawda? Jedyną standardową klasą łańcuchów, o której wiedziałem wcześniej w C ++, było std :: string. Istnieje pewne wsparcie dla używania znaków char * jako „ciągów” dla wstecznej kompatybilności z C, ale prawie nigdy nie muszę tego używać. Jasne, istnieje wiele zdefiniowanych przez użytkownika klas stron trzecich dla prawie wszystkiego, co możesz sobie wyobrazić, i prawdopodobnie są w to zawarte łańcuchy, ale prawie nigdy nie muszę ich używać.
TED
@TED: To, że „prawie nigdy nie musisz ich używać”, nie oznacza, że inni ludzie nie używają ich rutynowo. string_viewjest typem lingua franca, który może współpracować ze wszystkim.
Nicol Bolas
3
@TED: Dlatego powiedziałem „C ++ jako środowisko programistyczne”, w przeciwieństwie do „C ++ jako język / biblioteka”.
Nicol Bolas
2
@TED: „ Więc mógłbym równie dobrze powiedzieć„ C ++, ponieważ środowisko programistyczne ma tysiące klas kontenerów ”? ” I tak. Ale mogę pisać algorytmy, które działają z iteratorami, i wszystkie klasy kontenerów zgodne z tym paradygmatem będą z nimi współpracować. Natomiast „algorytmy”, które mogą przyjmować dowolną ciągłą tablicę znaków, były znacznie trudniejsze do napisania. Dzięki string_viewjest to łatwe.
Nicol Bolas,
1
@TED: Tablice znaków są bardzo szczególnym przypadkiem. Są niezwykle powszechne, a różne kontenery ciągłych znaków różnią się tylko sposobem zarządzania pamięcią, a nie sposobem iteracji danych. Tak więc sensowne jest posiadanie jednego rodzaju zakresu lingua franca, który może obejmować wszystkie takie przypadki bez konieczności stosowania szablonu. Uogólnienie poza tym leży w gestii Range TS i szablonów.
Nicol Bolas,