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 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 тихо проигнорируется. Иногда это допустимо, но чаще приводит к «странным» сценариям, где пользователь уверен, что ввёл ещё один параметр, а программа делает вид, что его не было.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ