JavaRush /Курсы /C++ SELF /std::quoted: чтение и запись строк в кавычках

std::quoted: чтение и запись строк в кавычках

C++ SELF
17 уровень , 3 лекция
Открыта

1. Пробелы, токены и кавычки

Если вы раньше писали интерактивные программы, у вас уже мог быть опыт: «Я ввёл название товара milk chocolate, а программа прочитала только milk». Это не баг компилятора и не злая магия. Это честное правило operator>>: по умолчанию он делит поток по пробельным символам (пробел, табуляция, перевод строки). Для чисел это обычно прекрасно, а вот для «названий», «сообщений», «адресов» — уже боль.

Представьте, что мы делаем мини‑CLI (консольное меню), где пользователь вводит команды:

  • add milk 2
  • add milk chocolate 2

Во втором случае формат становится двусмысленным: milk — это название? или первая часть названия? или команда другая? Потоковый парсер не умеет «догадываться» — он просто читает токены.

Классическое решение из мира текстовых форматов: если поле может содержать пробелы, мы берём его в кавычки. Тогда контракт становится чётким:

  • команда: слово без пробелов
  • название: строка в кавычках, внутри могут быть пробелы
  • количество: число

То есть: add "milk chocolate" 2.

Осталось научиться это удобно читать. Можно вручную резать строку, искать первую кавычку, последнюю, вынимать подстроку… но сегодня мы сделаем проще: попросим стандартную библиотеку сделать эту работу за нас.

2. std::quoted: что это и где подключать

Когда вы видите в C++ что-то вроде std::fixed, std::setprecision, std::setw — это манипуляторы потока: небольшие объекты, которые меняют поведение ввода/вывода. std::quoted — из этой же семьи.

Технически std::quoted — манипулятор, который можно вставлять в цепочки << и >>.

  • При выводе он печатает строку в кавычках и экранирует специальные символы (обычно кавычки и обратный слэш).
  • При вводе он умеет прочитать строку в кавычках как один логический токен, снимая внешние кавычки и корректно обрабатывая экранирование.

Чтобы пользоваться std::quoted, нужен заголовок:

#include <iomanip>   // std::quoted

И, конечно, потоки:

#include <iostream>
#include <sstream>   // если читаем из строки, через istringstream
#include <string>

Мини‑проверка «оно вообще существует»:

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

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

Обратите внимание: даже если в строке нет пробелов, std::quoted всё равно добавляет кавычки. Это нормально: мы явно говорим «выведи в quoted‑формате».

3. Вывод: делаем строку безопасной

Когда мы проектируем текстовый формат, нам нужно уметь не только читать, но и генерировать строки в том же формате. И вот тут std::quoted особенно приятен: он даёт симметрию «запись ↔ чтение». Вы записали строку в кавычках, потом прочитали её обратно — и получили то же значение (при разумных ограничениях).

Начнём с простого: строка с пробелами.

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

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

Теперь важнее: строка с кавычками внутри. Если вы делаете названия вроде Bob said "hi" — без экранирования формат развалится: где кавычки открывают поле, а где это просто символ внутри поля?

std::quoted при выводе экранирует кавычки обратным слэшем.

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

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

И здесь появляется важная мысль (мы к ней ещё вернёмся): в этом примере два разных уровня экранирования. Внутри C++‑кода вы написали \", потому что так устроены строковые литералы языка. А std::quoted печатает \", потому что так устроен текстовый формат данных. Похожие символы, но смысл разный.

Если вы собираете строку «как команду» для логов или для сохранения куда‑то (пока — просто в память), удобно использовать 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
}

Мы пока ничего «не сохраняем в файл» (это другой день курса), но уже учимся строить корректные строки формата.

4. Ввод: читаем quoted‑строку как один токен

Теперь самое практичное: как сделать так, чтобы строка "milk chocolate" считалась одним полем, а не двумя словами milk и chocolate.

Схема такая: вы читаете из потока как обычно, но для строки используете 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";
    }
}

Здесь важно привыкнуть к дисциплине: не использовать значения, пока вы не проверили, что чтение успешно. У новичков часто бывает так: они пишут iss >> cmd >> std::quoted(name) >> count;, а потом печатают name, даже если ввод был неправильный. Поток в случае ошибки просто перейдёт в состояние fail, а переменные могут остаться старыми или частично заполненными — и начинается сериал «почему иногда работает».

Ещё один момент: std::quoted(name) ожидает, что в потоке действительно будет строка в кавычках (по умолчанию — в двойных "). Если кавычек нет, чтение, как правило, не пройдёт так, как вы ожидаете по контракту. И это хорошо: формат нарушен — значит, мы должны честно сказать "bad format", а не пытаться угадывать.

5. Два уровня экранирования: данные и C++‑код

Сейчас будет участок лекции, где у половины группы появится выражение лица «я всё понял… но ничего не понял». Это нормально: тут реально два слоя, и мозг сначала пытается смешать их в один.

Первый слой — экранирование в строковом литерале C++. Это то, что вы пишете в исходнике:

  • "\n" — один символ перевода строки
  • "\"" — одна кавычка
  • "\\ " — один обратный слэш (и пробел)

Второй слой — экранирование внутри формата данных, который вы парсите. Например, в quoted‑формате кавычка внутри строки должна быть записана как \", иначе она закроет строку раньше времени.

Давайте посмотрим на пример, где в данных есть кавычки.

Данные (как пользователь мог бы ввести в CLI):

add "milk \"dark\"" 1

То есть название товара: milk "dark".

Но если мы хотим положить эту строку в C++‑литерал (для теста), нам придётся экранировать уже по правилам 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"
    }
}

Посмотрите внимательно: в C++‑коде написано \\\". Это означает «в строке данных будет \"». А \" уже означает «кавычка внутри quoted‑строки».

Хорошая новость в том, что в реальной жизни эти строки чаще приходят не из исходника, а от пользователя/из файла/из сети — и там остаётся только один уровень экранирования (уровень формата данных).

Также полезно понимать, что std::quoted при чтении «съедает» внешние кавычки и «разэкранирует» внутренние последовательности. То есть в name вы получите уже «чистую» строку для работы.

6. Мини‑CLI «Список покупок»

Чтобы std::quoted не остался «прикольной штукой из лекции», давайте встроим его в маленькое консольное приложение. Мы не используем struct (его у нас ещё нет по плану), поэтому сделаем простой «параллельный» хранитель: vector<string> для названий и vector<int> для количества. Да, это не идеальная архитектура, но сейчас наша цель — парсинг, а не дизайн модели данных.

Начнём с хранения и печати списка:

#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';
    }
}

Теперь сделаем парсер команды add. Он принимает строку целиком, пытается извлечь команду, quoted‑название и количество.

#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); // никаких лишних токенов
}

Обратите внимание на последнюю проверку. Она делает наш контракт строже: если пользователь ввёл add "milk" 2 blah, мы считаем это ошибкой формата, а не молча игнорируем хвост. Иногда «best effort» полезен, но для учебного парсинга строгий режим проще отлаживать.

Теперь — обработчик команды. Если товара ещё нет, добавим новую позицию. Если уже есть — увеличим количество. Для поиска сделаем маленькую функцию:

#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;
}

И применим её:

#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;
    }
}

Теперь соберём «цикл чтения команд». Мы читаем строку целиком через std::getline, потому что команды могут содержать пробелы внутри кавычек (и это как раз наша тема).

#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";
        }
    }
}

Теперь у нас реально работает формат:

  • add "milk chocolate" 3
  • add "milk \"dark\"" 1 (если пользователь умеет экранировать кавычки в данных)
  • list
  • exit

И главное: мы парсим название как одно поле, даже если там пробелы.

7. Формат как контракт

На этом этапе важно не «угадывать», что хотел пользователь, а чётко понимать, что мы считаем валидным. Потому что std::quoted — инструмент строгий: он не лечит хаос, он помогает вам поддерживать договорённость о формате.

Давайте запишем контракт нашей команды add в виде небольшой таблицы:

Поле Пример Как читаем Комментарий
Команда
add
std::string cmd; iss >> cmd;
Без пробелов, одно слово
Название
"milk chocolate"
iss >> std::quoted(name);
Обязательно в кавычках, пробелы внутри разрешены
Количество
3
iss >> count;
Целое число

Из этого автоматически следует, что строка add milk chocolate 3 для нас невалидна, даже если «по смыслу понятно». Потому что по контракту «название — в кавычках». И это хорошее правило: программа не должна играть в телепата.

Для наглядности можно представить разбор как маленькую блок‑схему:

flowchart TD
    A[Взяли строку line] --> B[iss >> cmd]
    B --> C{cmd == 'add'?}
    C -- нет --> X[Это не add: другая команда]
    C -- да --> D["iss >> quoted(name)"]
    D --> E[iss >> count]
    E --> F{успех чтения?}
    F -- нет --> Y[bad format]
    F -- да --> G{есть лишние токены?}
    G -- да --> Y
    G -- нет --> H["addItem(name, count)"]

Если вы умеете так «рисовать» процесс у себя в голове, вы начинаете отлаживать парсеры в разы быстрее.

Другие кавычки и escape‑символ

Иногда формат данных диктует не двойные кавычки, а, например, одинарные. Или вы по какой‑то причине хотите экранировать не \, а другим символом. std::quoted это позволяет: у него есть форма с параметрами «кавычка» и «escape‑символ».

Выглядит так:

std::quoted(str, quote_char, escape_char)

Пример: читаем строку в одинарных кавычках '...':

#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
    }
}

Почему это может быть полезно? Иногда данные уже существуют в таком формате (например, вы копируете ввод из какой‑то системы), и вам проще принять их правила, чем заставлять пользователя переучиваться. Но тут важно не переборщить: чем больше вариантов формата вы поддерживаете, тем больше угловых случаев вам придётся отлаживать.

Ещё одна тонкость: если вы меняете escape‑символ, вы обязаны понимать, как теперь будут выглядеть экранированные кавычки внутри данных. То есть вы буквально меняете «язык» мини‑формата.

8. Типичные ошибки при работе со std::quoted

Ошибка №1: забыли подключить <iomanip>.
Самая простая и обидная ситуация: вы пишете std::quoted(name), а компилятор сообщает, что такого не знает. Интуитивно хочется обвинить <sstream> или <iostream>, но quoted живёт именно в <iomanip>, потому что это манипулятор ввода/вывода.

Ошибка №2: ожидают, что std::quoted «сам догадается» и без кавычек.
Если по контракту поле может содержать пробелы, кавычки — не украшение, а часть формата. std::quoted не должен угадывать, где заканчивается строка, если кавычек нет. Поэтому ввод add milk chocolate 3 — это не «почти правильно», а просто другой формат.

Ошибка №3: путают экранирование в данных и экранирование в C++‑литерале.
Когда вы тестируете парсер через std::string line = "...";, вам нужно экранировать обратные слэши и кавычки по правилам C++ — и это создаёт ощущение, что std::quoted «требует странных последовательностей». На самом деле в реальном вводе от пользователя будет только один уровень экранирования — формат данных.

Ошибка №4: читают без проверки и используют переменные «как будто всё получилось».
Очень частая логика: «ну я же ввёл правильно, значит и программа прочитает». Но как только появится лишний пробел, пустая строка, забытая кавычка или буква вместо числа, поток перейдёт в fail, и ваши переменные окажутся частично заполненными. Правило простое: сначала if (iss >> ...), потом работа с результатом.

Ошибка №5: не проверяют хвост строки и случайно принимают мусор.
Если вы не проверяете, что после чтения всех полей поток закончился, то add "milk" 2 blah будет считаться корректной командой, а blah тихо проигнорируется. Иногда это допустимо, но чаще приводит к «странным» сценариям, где пользователь уверен, что ввёл ещё один параметр, а программа делает вид, что его не было.

1
Задача
C++ SELF, 17 уровень, 3 лекция
Недоступна
Безопасная цитата
Безопасная цитата
1
Задача
C++ SELF, 17 уровень, 3 лекция
Недоступна
Значение в кавычках
Значение в кавычках
1
Задача
C++ SELF, 17 уровень, 3 лекция
Недоступна
Команда добавления
Команда добавления
1
Задача
C++ SELF, 17 уровень, 3 лекция
Недоступна
Список тегов
Список тегов
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ