CodeGym /Kursy /C++ SELF /Raw‑literały R"( ... )"

Raw‑literały R"( ... )"

C++ SELF
Poziom 17, Lekcja 4
Dostępny

1. Czasem escape zaczyna irytować

Jeśli jeszcze nie zdążyłeś znienawidzić escape’owania — nie martw się, po prostu jeszcze za mało pisałeś „realistycznych” stringów. Problem zwykle pojawia się nie przy std::cout << "Hello";, lecz przy stringach, które wyglądają jak dane: ścieżka do pliku, fragment JSON‑a, linia poleceń, wyrażenie regularne, „tekst pomocy” z cudzysłowami. W rezultacie źródło zamienia się w „makaron ze slashy” i zaczynasz się zastanawiać, ile tam powinno być \\ i gdzie dokładnie wpisać \".

Aby poczuć ból, wystarczy przypomnieć sobie, że zwykły literał stringowy interpretuje sekwencje escape: \n, \t, \", \\. To wygodne, dopóki string jest krótki i „czysty”. Ale jeśli twój string sam zawiera slashe i cudzysłowy jako znaki danych, musisz „escape’ować escape’owanie”. Właśnie wtedy raw‑stringi przestają być luksusem, a stają się normą sanity dla programisty.

Surowy (raw) string robi prostą rzecz: pozwala zapisać tekst prawie tak, jak wygląda w rzeczywistości, i kompilator nie próbuje interpretować \n jako przejścia do nowej linii i nie zmusza cię do escape’owania każdego cudzysłowu wewnątrz.

Zwykłe stringi i sekwencje escape

Przed raw‑stringami warto na chwilę przypomnieć, co się dzieje w zwykłym stringu. Naszym celem nie jest nauka wszystkich kodów escape, a zrozumienie przyczyny, dla której string czasem wygląda jak „dywan z odwrotnych slashy”.

W zwykłym literału stringowym "\n" — to jeden znak nowej linii, a "\\n" — dwa znaki: backslash i litera n. Podobnie, żeby w stringu umieścić cudzysłów ", trzeba pisać \". A jeżeli string to ścieżka w Windows, to C:\temp\data.txt nagle zaczyna wyglądać jak „coś z tabulacją”, bo \t — to tab.

#include <iostream>
#include <string>

int main() {
    std::string path = "C:\\temp\\data.txt";
    std::cout << path << '\n'; // C:\temp\data.txt
}

Ta forma jest poprawna, ale nieprzyjemna do czytania: string w kodzie nie przypomina stringu „w życiu”. A teraz wyobraź sobie, że w stringu są jeszcze cudzysłowy, kilka linii tekstu i przykłady poleceń. Właśnie wtedy raw‑stringi ratują psychikę (i zmniejszają szansę na błąd).

2. Raw‑string R"( ... )": składnia i idea „jak jest”

Raw‑string to literał stringowy, w którym zawartość jest traktowana dosłownie. To znaczy backslash \ przestaje być „magiczny” dla sekwencji escape i staje się zwykłym znakiem. Z tego wynika miły bonus: cudzysłowy wewnątrz stringu zazwyczaj można pisać bez \" (ponieważ raw‑string nie zamyka się cudzysłowem wewnątrz, lecz specjalną sekwencją).

Podstawowa składnia wygląda tak:

R"(tekst)"

Wewnątrz nawiasów można pisać prawie dowolny tekst, włącznie z cudzysłowami i backslashami. Ważne, że raw‑stringi to część języka C++, a nie biblioteka: nie są potrzebne żadne nagłówki. W standardzie to dokładnie raw string literal — odrębny rodzaj literału stringowego.

Porównajmy zwykły string i raw‑string na przykładzie ścieżki:

#include <iostream>
#include <string>

int main() {
    std::string normal = "C:\\temp\\data.txt";
    std::string raw = R"(C:\temp\data.txt)";

    std::cout << normal << '\n'; // C:\temp\data.txt
    std::cout << raw << '\n';    // C:\temp\data.txt
}

Oba stringi dają ten sam rezultat, ale wariant raw czyta się jak normalna, ludzka ścieżka, a nie test skupienia.

3. Niuanse raw‑stringów w prawdziwym kodzie

\n — to nie jest nowa linia, dopóki jej nie wpiszesz

Tu zwykle pojawia się pierwsze „zasadzka” dla początkujących: raw‑string nie „rozpoznaje” sekwencji escape, więc \n wewnątrz niego to dwa znaki: \ i n. Jeśli chcesz prawdziwy znak końca linii, musisz… naprawdę nacisnąć Enter w tekście raw‑stringa. Brzmi to prosto, ale przez przyzwyczajenie do "\n" wielu otrzymuje nieoczekiwany wynik.

Pokażmy to najuczciwiej:

#include <iostream>
#include <string>

int main() {
    std::string a = "A\nB";
    std::string b = R"(A\nB)";

    std::cout << a << '\n'; // A (nowa linia) B
    std::cout << b << '\n'; // A\nB
}

I tu ważne, by nie pomylić celów. Kiedy w źródle chcesz, żeby były znaki \ i n (np. pokazujesz użytkownikowi przykład polecenia), raw‑string jest idealny. Gdy chcesz faktycznie wstawić znak tabulacji lub nowej linii w jednym stringu, zwykły literał albo konkatenacja z '\n' często będzie wygodniejsza.

Wieloliniowe raw‑stringi: pomoc i „dane wbudowane”

Jeśli kiedykolwiek pisałeś komunikat help dla programu konsolowego, wiesz: na początku wszystko jest ładne, a potem dokłada się przykłady z cudzysłowami i backslashami — i zaczyna się „feta escape’owania”. Raw‑string pozwala trzymać taki tekst w źródle „jak w dokumentacji”, z normalnymi złamaniami linii.

Wyobraźmy sobie, że kontynuujemy nasze proste CLI‑aplikację „Lista zakupów”. W poprzednich wykładach potrafiliśmy już parsować polecenie typu add "milk chocolate" 3 przez std::istringstream i std::quoted. Teraz chcemy dodać komendę help, która wypisuje instrukcję.

#include <iostream>
#include <string>

int main() {
    const std::string help = R"(Commands:
  add "name with spaces" count
  list
Example:
  add "milk chocolate" 3
Windows path example: C:\temp\shop.txt
)";

    std::cout << help; // wypisuje wieloliniowy tekst
}

Tekst wygląda „jak napisał człowiek”, a nie „jak zmusił kompilator”.

A teraz drugi użyteczny scenariusz: chcesz osadzić w kodzie mały zestaw danych testowych. Na przykład „mini‑skrypt”, który symuluje wejście użytkownika, żeby szybko przetestować parser. To szczególnie wygodne przy wyjaśnianiu tematu albo sprawdzaniu logiki bez ręcznego wpisywania.

Schemat jest taki:

flowchart TD
    A["raw-string: scenariusz wejścia"] --> B["std::istringstream"]
    B --> C["std::getline: czytamy linia po linii"]
    C --> D["parseCommand(line): parsujemy jedno polecenie"]
    D --> E["akcja: add/list/help"]

A kod „skryptu” można trzymać bezpośrednio w raw‑stringu:

#include <iostream>
#include <sstream>
#include <string>

int main() {
    const std::string script = R"(add "milk chocolate" 3
add "green tea" 1
list
)";
    std::istringstream input(script);
    std::string line;

    while (std::getline(input, line)) {
        std::cout << ">> " << line << '\n'; // >> add "milk chocolate" 3 ...
    }
}

Zauważ, nie omawiamy tu plików ani tworzenia „prawdziwych” testów — to przyjdzie później w kursie. Na razie to wygodny sposób, by nie męczyć się ręcznym wprowadzaniem danych przy debugowaniu parsera stringów.

Własny delimiter: R"TAG( ... )TAG"

Raw‑string ma jedno ograniczenie, o którym warto wiedzieć wcześniej, żeby później nie kłócić się z kompilatorem. Podstawowy raw‑string zamyka się sekwencją )". To znaczy, jeśli wewnątrz tekstu przypadkowo pojawi się )", kompilator może uznać, że string skończył się wcześniej i dalej zacznie się „parada błędów”.

Aby rozwiązać ten problem, C++ pozwala ustawić własny „tag” (delimiter). Wtedy string otwierasz jako R"TAG( a zamykasz jako )TAG". Wybierasz TAG tak, aby na pewno nie występował w tekście.

#include <iostream>
#include <string>

int main() {
    std::string s = R"TAG(This text contains )" inside, so we use TAG.)TAG";
    std::cout << s << '\n'; // This text contains )" inside, so we use TAG.
}

Wygląda to nietypowo, ale działa bardzo logicznie: „otworzyliśmy z TAG — zamykamy z TAG”. Ten wariant ratuje sytuację, gdy trzymasz w stringu fragment kodu, JSON‑a lub tekst, gdzie )" może się pojawić.

Ponieważ raw‑stringi są częścią leksyki języka (na poziomie „jak kompilator czyta tokeny”), standard C++ traktuje je poważnie i szczegółowo określa kwestie rozpoznawania znaku R przy raw‑literałach oraz związane z tym niuanse w sekcji leksyki.

Raw‑stringi i std::quoted: różne poziomy zadania

Częsta pomyłka początkujących brzmi: „Przecież znam std::quoted, po co mi raw‑stringi?” Później pojawia się druga: „Napisałem raw‑string, więc std::quoted nie jest potrzebne?” Oba stwierdzenia są błędne — to narzędzia do różnych zastosowań.

Raw‑string dotyczy tego, jak definiujesz string w źródle C++. Przydaje się do literałów, pomocy, danych testowych. std::quoted dotyczy tego, jak czytasz i zapisujesz stringi w formacie danych, gdy zgodnie z kontraktem string jest ujęty w cudzysłowy, aby mógł zawierać spacje. Te dwa poziomy łatwo pomylić, bo w obu pojawiają się cudzysłowy.

Krótka tabela, żeby utrwalić różnicę:

Pytanie Raw‑string R"( ... )" std::quoted
Gdzie używany? W źródle C++ (literał) W strumieniowym wejściu/wyjściu danych
Co rozwiązuje? Usuwa escape’owanie \\, \", zachowuje złamania linii Odczytuje/zapisuje string w cudzysłowie jako jeden token
Czy „rozumie” \n? Nie, to są dwa znaki: \ i n To zależy od danych; std::quoted pracuje z escape’owaniem zgodnie z formatem
Czy potrzebny nagłówek? Nie (konstrukcja językowa) Tak, <iomanip>

I przykład, gdzie współpracują: raw‑string pozwala wygodnie zapisać testowy string, a std::quoted — prawidłowo go sparsować.

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

int main() {
    std::string line = R"(add "milk chocolate" 3)";
    std::istringstream iss(line);

    std::string cmd, name;
    int count{};

    if (iss >> cmd >> std::quoted(name) >> count) {
        std::cout << name << " x" << count << '\n'; // milk chocolate x3
    }
}

Tutaj raw‑string sprawia, że test jest czytelny. Parsowanie robi std::quoted.

4. Osadzanie raw‑stringów w CLI: help i demo‑skrypt

Zrobimy teraz mały, ale praktyczny krok: dodamy do naszej aplikacji dwie rzeczy — wieloliniową pomoc i „demo‑wejście” do debugowania. Zwróć uwagę, że nie wchodzimy tu w architekturę, pliki, klasy i inne duże tematy. Zostajemy w ramach dzisiejszego dnia: stringi, strumienie, getline, quoted.

Zacznijmy od funkcji zwracającej tekst pomocy. W prawdziwym projekcie wyodrębniłbyś to bardziej porządnie, ale teraz celem jest trening raw‑stringów.

#include <string>

std::string BuildHelpText() {
    return R"(Commands:
  add "name" count
  list
  help
Example:
  add "milk chocolate" 3
)";
}

Teraz w main możesz wypisywać pomoc po komendzie help. Uprośćmy: na razie porównamy całą linię (znamy już if/else i getline).

#include <iostream>
#include <string>

std::string BuildHelpText();

int main() {
    std::string line;
    std::getline(std::cin, line);

    if (line == "help") {
        std::cout << BuildHelpText();
    }
}

Następny krok — demo‑skrypt. Wyobraźmy sobie, że chcemy „odtworzyć” kilka komend bez ręcznego wpisywania. Zrobimy zmienną demoInput, która będzie std::istringstream, stworzonym z raw‑stringa. To bardzo wygodne na zajęciach: ten sam kod — i wszyscy studenci mają ten sam scenariusz.

#include <sstream>
#include <string>

std::istringstream BuildDemoInput() {
    const std::string script = R"(add "milk chocolate" 3
add "green tea" 1
list
)";
    return std::istringstream(script);
}

A tak możesz czytać komendy linia po linii z takiego strumienia:

#include <iostream>
#include <sstream>
#include <string>

std::istringstream BuildDemoInput();

int main() {
    auto input = BuildDemoInput();
    std::string line;

    while (std::getline(input, line)) {
        std::cout << ">> " << line << '\n'; // >> add "milk chocolate" 3 ...
    }
}

Jest tu subtelność, która w praktyce ułatwia życie: raw‑string zachowuje znaki nowej linii dokładnie tak, jak są, i nie musisz pisać "...\n...\n" i pilnować cudzysłowów. Powstaje „mini‑plik” osadzony bezpośrednio w źródle, który czytasz tak samo jak normalne wejście.

Jeśli później podłączysz swój parser komend (z poprzednich wykładów, gdzie używaliśmy std::istringstream + std::quoted), to ten demo‑skrypt będzie idealnym sposobem szybkiego sprawdzenia, że parsowanie się nie zepsuło po zmianach.

5. Typowe błędy przy pracy z raw‑stringami

Błąd nr 1: oczekiwanie, że \n wewnątrz raw‑stringa stanie się znakiem nowej linii.
W zwykłych stringach "\n" — to specjalny znak nowej linii i mózg szybko się do tego przyzwyczaja. W raw‑stringu R"(\n)" — to dosłownie dwa znaki: backslash i litera n. Jeśli potrzebujesz prawdziwego złamania linii, wstaw je fizycznie w treści raw‑literału.

Błąd nr 2: „dlaczego kompilator krzyczy w środku stringa?” — przypadkowe )" wewnątrz tekstu.
Raw‑string zamyka się sekwencją )", i jeśli twój tekst ją zawiera (np. fragment kodu lub danych), kompilator uzna, że literał się skończył, a dalej zacznie się chaos z „nieoczekiwanymi tokenami”. Rozwiązuje to użycie własnego delimiteru: R"TAG( ... )TAG".

Błąd nr 3: zamiana poziomów — raw‑string zamiast std::quoted lub odwrotnie.
Raw‑string rozwiązuje problem zapisu w źródle, a nie problem odczytu z danych wejściowych. Jeśli użytkownik wpisuje add milk chocolate 3, raw‑string nie pomoże odczytać „milk chocolate” jako jednego tokenu — to zadanie dla std::quoted i ustalonego formatu („stringi z spacjami w cudzysłowach”). I odwrotnie, std::quoted nie sprawi, że twoje źródło stanie się bardziej czytelne, jeżeli w kodzie próbujesz zapisać tekst z C:\temp\....

Błąd nr 4: wybór delimiteru, który występuje w tekście.
Czasem studenci używają TAG, a potem wklejają tekst do raw‑stringa, gdzie pojawia się )TAG". Efekt identyczny jak bez delimiteru: string „nagle” się zamyka. Zasada prosta: wybieraj delimiter unikalny dla danego bloku (nawet R"__HELP__( ... )__HELP__"), i nie bój się, żeby był dziwny — lepiej nieładny i pewny niż ładny i zawodny.

Błąd nr 5: próba używania raw‑stringa tam, gdzie potrzebne są znaki sterujące.
Raw‑stringi są świetne do „tekstu jak jest”, ale jeżeli potrzebujesz zbudować string z tabulatorami, nowymi liniami, sygnałem dźwiękowym (tak, zdarza się), to zwykły literał z \t, \n może być prostszy. Raw‑string to narzędzie, nie religia: czasem slashe są przydatne.

1
Zadanie
C++ SELF, poziom 17, lekcja 4
Niedostępne
Ścieżka w kodzie
Ścieżka w kodzie
1
Zadanie
C++ SELF, poziom 17, lekcja 4
Niedostępne
Nowa linia czy tekst
Nowa linia czy tekst
1
Zadanie
C++ SELF, poziom 17, lekcja 4
Niedostępne
Wbudowana pomoc
Wbudowana pomoc
1
Zadanie
C++ SELF, poziom 17, lekcja 4
Niedostępne
Skrypt demonstracyjny
Skrypt demonstracyjny
1
Ankieta/quiz
Parsowanie ciągów znaków, poziom 17, lekcja 4
Niedostępny
Parsowanie ciągów znaków
Parsowanie ciągów znaków
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION