CodeGym /Kursy /C++ SELF /std::quoted: odczyt i zapis łańcuchów w cudzysłowie

std::quoted: odczyt i zapis łańcuchów w cudzysłowie

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

1. Spacje, tokeny i cudzysłowy

Jeśli wcześniej pisałeś programy interaktywne, mogłeś mieć doświadczenie: „Wpisałem nazwę produktu milk chocolate, a program odczytał tylko milk”. To nie błąd kompilatora i nie zła magia. To uczciwe reguły operator>>: domyślnie dzieli on strumień według znaków białych (spacja, tabulacja, nowa linia). Dla liczb to zazwyczaj okej, ale dla „nazw”, „wiadomości”, „adresów” — to problem.

Wyobraź sobie, że tworzymy mini‑CLI (menu konsolowe), gdzie użytkownik wpisuje polecenia:

  • add milk 2
  • add milk chocolate 2

W drugim przypadku format staje się dwuznaczny: milk — to nazwa? czy pierwsza część nazwy? czy inne polecenie? Parser strumieniowy nie potrafi „zgadywać” — po prostu czyta tokeny.

Klasyczne rozwiązanie ze świata formatów tekstowych: jeśli pole może zawierać spacje, otaczamy je cudzysłowami. Wtedy kontrakt staje się jednoznaczny:

  • polecenie: słowo bez spacji
  • nazwa: łańcuch w cudzysłowie, wewnątrz mogą być spacje
  • ilość: liczba

Czyli: add "milk chocolate" 2.

Pozostało nauczyć się tego wygodnie czytać. Można ręcznie ciąć string, szukać pierwszego cudzysłowu, ostatniego, wycinać podłańcuch… ale dziś zrobimy prościej: poprosimy standardową bibliotekę, aby zrobiła to za nas.

2. std::quoted: co to jest i gdzie dołączyć

Gdy widzisz w C++ coś w stylu std::fixed, std::setprecision, std::setw — to manipulatory strumieniowe: niewielkie obiekty, które zmieniają zachowanie wejścia/wyjścia. std::quoted należy do tej samej rodziny.

Technicznie std::quoted to manipulator, który można wkładać w łańcuchy << i >>.

  • Przy wypisywaniu drukuje łańcuch w cudzysłowach i escapuje znaki specjalne (zwykle cudzysłowy i odwrotny backslash).
  • Przy wczytywaniu potrafi odczytać łańcuch w cudzysłowie jako jeden logiczny token, usuwając zewnętrzne cudzysłowy i poprawnie obsługując escapowanie.

Aby używać std::quoted, potrzebny jest nagłówek:

#include <iomanip>   // std::quoted

I, oczywiście, strumienie:

#include <iostream>
#include <sstream>   // jeśli czytamy ze stringa, przez istringstream
#include <string>

Mini‑sprawdzenie „czy w ogóle istnieje”:

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

int main() {
    std::string s = "hello world";
    std::cout << std::quoted(s) << '\n'; // "hello world"
}

Zauważ: nawet jeśli w łańcuchu nie ma spacji, std::quoted i tak doda cudzysłowy. To normalne: wyraźnie mówimy „wypisz w formacie quoted”.

3. Wypisywanie: zabezpieczamy łańcuch

Kiedy projektujemy format tekstowy, musimy umieć nie tylko czytać, ale i generować łańcuchy w tym samym formacie. I tu std::quoted jest szczególnie przyjemny: daje symetrię „zapis ↔ odczyt”. Wypisałeś łańcuch w cudzysłowie, potem odczytałeś go z powrotem — i otrzymałeś tę samą wartość (przy rozsądnych ograniczeniach).

Zacznijmy od prostego: łańcuch ze spacjami.

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

int main() {
    std::string item = "milk chocolate";
    std::cout << std::quoted(item) << '\n'; // "milk chocolate"
}

Teraz ważniejsze: łańcuch z cudzysłowami wewnątrz. Jeśli robisz nazwy w rodzaju Bob said "hi" — bez escapowania format się rozsypie: gdzie cudzysłowy otwierają pole, a gdzie to zwykły znak wewnątrz pola?

std::quoted przy wypisywaniu escapuje cudzysłowy backslashem.

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

int main() {
    std::string s = "Bob said \"hi\"";
    std::cout << std::quoted(s) << '\n'; // "Bob said \"hi\""
}

I tu pojawia się ważna myśl (do której wrócimy): w tym przykładzie są dwa różne poziomy escapowania. W kodzie C++ napisałeś \", bo tak działają literały łańcuchowe. A std::quoted wypisuje \", bo taki jest format danych. Podobne sekwencje, ale różne znaczenie.

Jeśli budujesz łańcuch „jako polecenie” do logów albo do zapisu, wygodnie użyć std::ostringstream:

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

int main() {
    std::string name = "milk chocolate";
    int count = 3;

    std::ostringstream oss;
    oss << "add " << std::quoted(name) << ' ' << count;

    // oss.str() == add "milk chocolate" 3
}

Na razie nic „nie zapisujemy do pliku” (to inny dzień kursu), ale uczymy się budować poprawne łańcuchy formatu.

4. Odczyt: czytamy quoted‑łańcuch jako jeden token

Teraz praktyczna część: jak sprawić, żeby łańcuch "milk chocolate" był jednym polem, a nie dwoma słowami milk i chocolate.

Schemat jest taki: czytasz ze strumienia jak zwykle, ale dla łańcucha używasz std::quoted(str).

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

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

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

    if (iss >> cmd >> std::quoted(name) >> count) {
        std::cout << cmd << " | " << name << " | " << count << '\n';
        // add | milk chocolate | 3
    } else {
        std::cout << "bad format\n";
    }
}

Tu ważna dyscyplina: nie używać wartości, zanim nie sprawdzisz, że odczyt się powiódł. U początkujących często wygląda to tak, że piszą iss >> cmd >> std::quoted(name) >> count;, a potem wypisują name, nawet jeśli wejście było niepoprawne. Strumień w przypadku błędu przejdzie w stan fail, a zmienne mogą zostać stare lub częściowo wypełnione — i zaczyna się serial „dlaczego czasem działa”.

Jeszcze jedna kwestia: std::quoted(name) oczekuje, że w strumieniu rzeczywiście będzie łańcuch w cudzysłowie (domyślnie — podwójne "). Jeśli cudzysłowów nie ma, odczyt zazwyczaj nie pójdzie zgodnie z kontraktem. I to dobrze: format jest naruszony — powinniśmy uczciwie zgłosić "bad format", a nie próbować zgadywać.

5. Dwa poziomy escapowania: dane i kod C++

Teraz fragment wykładu, po którym połowa grupy zrobi minę „myślę, że rozumiem… ale nic nie rozumiem”. To normalne: są dwa poziomy i mózg najpierw próbuje je pomieszać.

Pierwszy poziom — escapowanie w literałach łańcuchowych C++. To, co piszesz w źródle:

  • "\n" — jeden znak nowej linii
  • "\"" — jeden cudzysłów
  • "\\ " — jeden backslash (i spacja)

Drugi poziom — escapowanie w formacie danych, który parsujesz. Na przykład w quoted‑formacie cudzysłów wewnątrz łańcucha powinien być zapisany jako \", inaczej zamknie łańcuch zbyt wcześnie.

Spójrzmy na przykład, gdzie w danych są cudzysłowy.

Dane (jak użytkownik mógłby wpisać w CLI):

add "milk \"dark\"" 1

Czyli nazwa produktu: milk "dark".

Ale jeśli chcemy umieścić ten łańcuch w literał C++ (do testu), musimy escapować zgodnie z regułami C++:

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

int main() {
    std::string line = "add \"milk \\\"dark\\\"\" 1";
    std::istringstream iss(line);

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

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

Zwróć uwagę: w kodzie C++ napisano \\\". To oznacza „w stringu danych będzie \"”. A \" oznacza już „cudzysłów wewnątrz quoted‑łańcucha”.

Dobra wiadomość jest taka, że w praktyce takie łańcuchy częściej pochodzą od użytkownika/z pliku/z sieci — i wtedy zostaje tylko jeden poziom escapowania (poziom formatu danych).

Warto też wiedzieć, że std::quoted przy czytaniu „zjada” zewnętrzne cudzysłowy i „deescapuje” wewnętrzne sekwencje. Czyli w name dostaniesz już „czysty” łańcuch do pracy.

6. Mini‑CLI „Lista zakupów”

Aby std::quoted nie został „fajnym gadżetem z wykładu”, wbudujmy go w małą aplikację konsolową. Nie używamy tu struct (jeszcze nie ma w planie), więc zrobimy prosty przechowywacz „równoległy”: vector<string> dla nazw i vector<int> dla ilości. Tak, to nie idealna architektura, ale teraz celem jest parsowanie, nie projekt modelu danych.

Zaczynamy od przechowywania i wypisywania listy:

#include <iostream>
#include <string>
#include <vector>

void printList(const std::vector<std::string>& names,
               const std::vector<int>& counts) {
    for (std::size_t i = 0; i < names.size(); ++i) {
        std::cout << i << ": " << names[i] << " x" << counts[i] << '\n';
    }
}

Teraz zrobimy parser polecenia add. Przyjmuje cały wiersz, próbuje wyciągnąć polecenie, quoted‑nazwę i ilość.

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

bool parseAdd(const std::string& line, std::string& name, int& count) {
    std::istringstream iss(line);
    std::string cmd;

    if (!(iss >> cmd >> std::quoted(name) >> count)) return false;
    if (cmd != "add") return false;

    std::string extra;
    return !(iss >> extra); // no extra tokens
}

Zwróć uwagę na ostatnią kontrolę. Uszczelnia nasz kontrakt: jeśli użytkownik wpisał add "milk" 2 blah, traktujemy to jako błąd formatu, zamiast cicho ignorować resztę. Czasami „best effort” jest przydatny, ale do ćwiczeń tryb ścisły ułatwia debugowanie.

Teraz — handler polecenia. Jeśli towaru jeszcze nie ma, dodajemy nową pozycję. Jeśli jest — zwiększamy ilość. Do wyszukiwania napiszemy małą funkcję:

#include <string>
#include <vector>

int findIndex(const std::vector<std::string>& names, const std::string& name) {
    for (std::size_t i = 0; i < names.size(); ++i) {
        if (names[i] == name) return static_cast<int>(i);
    }
    return -1;
}

I zastosujemy ją:

#include <string>
#include <vector>

void addItem(std::vector<std::string>& names, std::vector<int>& counts,
             const std::string& name, int count) {
    int idx = findIndex(names, name);
    if (idx == -1) {
        names.push_back(name);
        counts.push_back(count);
    } else {
        counts[static_cast<std::size_t>(idx)] += count;
    }
}

Teraz złożymy „pętlę czytania poleceń”. Czytamy cały wiersz przez std::getline, bo komendy mogą zawierać spacje wewnątrz cudzysłowów (to nasz temat).

#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> names;
    std::vector<int> counts;

    std::string line;
    while (std::getline(std::cin, line)) {
        if (line == "exit") break;

        std::string name;
        int count{};

        if (parseAdd(line, name, count)) {
            addItem(names, counts, name, count);
            std::cout << "ok\n";
        } else if (line == "list") {
            printList(names, counts);
        } else {
            std::cout << "unknown or bad format\n";
        }
    }
}

Teraz format naprawdę działa:

  • add "milk chocolate" 3
  • add "milk \"dark\"" 1 (jeśli użytkownik umie escapować cudzysłowy w danych)
  • list
  • exit

I najważniejsze: parsujemy nazwę jako jedno pole, nawet jeśli zawiera spacje.

7. Format jako kontrakt

Na tym etapie ważne jest, aby nie „zgadywać”, co użytkownik miał na myśli, lecz jasno określić, co uznajemy za poprawne. Ponieważ std::quoted to narzędzie surowe: nie leczy chaosu, pomaga utrzymać umówiony format.

Zapiszmy kontrakt naszego polecenia add w formie małej tabeli:

Pole Przykład Jak czytamy Komentarz
Polecenie
add
std::string cmd; iss >> cmd;
Bez spacji, jedno słowo
Nazwa
"milk chocolate"
iss >> std::quoted(name);
Obowiązkowo w cudzysłowie, spacje w środku dozwolone
Ilość
3
iss >> count;
Liczba całkowita

Z tego automatycznie wynika, że łańcuch add milk chocolate 3 jest dla nas niepoprawny, nawet jeśli „po sensie zrozumiały”. Bo według kontraktu „nazwa — w cudzysłowie”. To dobre założenie: program nie powinien grać w jasnowidzenie.

Dla przejrzystości można przedstawić analizę jako mały schemat blokowy:

flowchart TD
    A[Wzięto wiersz line] --> B[iss >> cmd]
    B --> C{cmd == 'add'?}
    C -- nie --> X[To nie add: inne polecenie]
    C -- tak --> D["iss >> quoted(name)"]
    D --> E[iss >> count]
    E --> F{powiodło się czytanie?}
    F -- nie --> Y[bad format]
    F -- tak --> G{są dodatkowe tokeny?}
    G -- tak --> Y
    G -- nie --> H["addItem(name, count)"]

Jeśli potrafisz tak „rysować” proces w głowie, debugowanie parserów przychodzi znacznie szybciej.

Inne cudzysłowy i znak escape

Czasem format danych narzuca nie podwójne cudzysłowy, lecz np. pojedyncze. Albo z jakiegoś powodu chcesz escapować nie za pomocą \, lecz innym znakiem. std::quoted to umożliwia: ma formę z parametrami „znak cudzysłowu” i „znak escape”.

Wygląda to tak:

std::quoted(str, quote_char, escape_char)

Przykład: czytamy łańcuch w pojedynczych cudzysłowach '...':

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

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

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

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

Dlaczego to może być przydatne? Czasem dane już istnieją w takim formacie (np. kopiujesz wejście z jakiegoś systemu) i łatwiej przyjąć ich reguły niż zmuszać użytkownika do przebrania się. Ale ważne, żeby nie przesadzić: im więcej wariantów formatu wspierasz, tym więcej krawędzi przypadków będziesz debugować.

Jeszcze jedna subtelność: jeśli zmieniasz znak escape, musisz rozumieć, jak teraz będą wyglądać escapowane cudzysłowy wewnątrz danych. Czyli dosłownie zmieniasz „język” mini‑formatu.

8. Typowe błędy przy pracy ze std::quoted

Błąd nr 1: zapomniano dołączyć <iomanip>.
Najprostsza i najbardziej przykra sytuacja: piszesz std::quoted(name), a kompilator mówi, że nie zna tej funkcji. Intuicyjnie chcesz obwiniać <sstream> lub <iostream>, ale quoted żyje właśnie w <iomanip>, bo to manipulator wejścia/wyjścia.

Błąd nr 2: oczekują, że std::quoted „sam zgadnie” i bez cudzysłowów.
Jeśli według kontraktu pole może zawierać spacje, cudzysłowy to nie ozdoba, lecz część formatu. std::quoted nie ma za zadanie zgadywać, gdzie kończy się łańcuch, jeśli brak cudzysłowów. Dlatego wejście add milk chocolate 3 to nie „prawie poprawne”, lecz inny format.

Błąd nr 3: mylą escapowanie w danych i w literałach C++.
Gdy testujesz parser przez przypisanie std::string line = "...";, musisz escapować backslashe i cudzysłowy według reguł C++ — i to sprawia wrażenie, że std::quoted „wymaga dziwnych sekwencji”. W rzeczywistym wejściu od użytkownika będzie tylko jeden poziom escapowania — format danych.

Błąd nr 4: czytają bez sprawdzenia i używają zmiennych „jakby wszystko się udało”.
Częsta logika: „w końcu wpisałem dobrze, więc program odczyta”. Ale gdy pojawi się dodatkowa spacja, pusta linia, brak cudzysłowu lub litera zamiast liczby, strumień przejdzie w stan fail, a twoje zmienne będą częściowo wypełnione. Prosta zasada: najpierw if (iss >> ...), potem pracuj z wynikiem.

Błąd nr 5: nie sprawdzają końca wiersza i przypadkowo akceptują śmieci.
Jeśli nie sprawdzasz, że po odczytaniu wszystkich pól strumień się skończył, to add "milk" 2 blah zostanie potraktowane jako poprawne polecenie, a blah zostanie cicho zignorowane. Czasami to dopuszczalne, ale częściej prowadzi do „dziwnych” scenariuszy, w których użytkownik myśli, że dodał kolejny parametr, a program robi jakby go nie było.

1
Zadanie
C++ SELF, poziom 17, lekcja 3
Niedostępne
Bezpieczny cytat
Bezpieczny cytat
1
Zadanie
C++ SELF, poziom 17, lekcja 3
Niedostępne
Wartość w cudzysłowie
Wartość w cudzysłowie
1
Zadanie
C++ SELF, poziom 17, lekcja 3
Niedostępne
Komenda dodawania
Komenda dodawania
1
Zadanie
C++ SELF, poziom 17, lekcja 3
Niedostępne
Lista tagów
Lista tagów
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION