JavaRush /Курсы /C++ SELF /Raw‑строки R"( ... )"

Raw‑строки R"( ... )"

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

1. Иногда экранирование начинает бесить

Если вы пока не успели возненавидеть экранирование — не переживайте, вы просто ещё мало писали “реалистичных” строк. Проблема обычно появляется не на std::cout << "Hello";, а на строках, которые похожи на данные: путь к файлу, JSON‑кусок, командная строка, регулярка, “текст справки” с кавычками. В итоге исходник превращается в “лапшу из слешей”, и вы начинаете сомневаться, сколько там должно быть \\ и где именно ставить \".

Чтобы почувствовать боль, достаточно вспомнить, что обычный строковый литерал обрабатывает escape‑последовательности: \n, \t, \", \\. Это удобно, пока строка короткая и “чистая”. Но если ваша строка сама содержит слеши и кавычки как символы данных, вам приходится “экранировать экранирование”. Именно в этот момент raw‑строки становятся не роскошью, а санитарной нормой программиста.

Сырая (raw) строка делает простую вещь: позволяет записать текст почти так, как он выглядит в реальности, и компилятор не пытается интерпретировать \n как перевод строки и не заставляет вас экранировать каждую кавычку внутри.

Обычные строки и escape‑последовательности

Перед raw‑строками полезно на минутку вспомнить, что происходит в обычной строке. Сейчас наша цель — не выучить все escape‑коды, а понять причину, почему строка иногда выглядит как “ковёр из обратных слешей”.

В обычном строковом литерале "\n" — это один символ перевода строки, а "\\n" — два символа: обратный слеш и буква n. Аналогично, чтобы внутри строки получить кавычку ", приходится писать \". И если строка — это путь в Windows, то C:\temp\data.txt внезапно начинает выглядеть как “что-то с табуляцией”, потому что \t — это таб.

#include <iostream>
#include <string>

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

Эта запись корректная, но глазами её читать неприятно: строка в коде не похожа на строку “в жизни”. А теперь представьте, что в строке ещё и кавычки, и несколько строк текста, и примеры команд. Вот здесь raw‑строки и приходят спасать психику (а заодно уменьшают шанс ошибки).

2. Raw‑строка R"( ... )": синтаксис и идея “как есть”

Raw‑строка — это строковый литерал, где содержимое воспринимается буквально. То есть обратный слеш \ перестаёт быть “магическим символом” для escape‑последовательностей и становится обычным символом. Из этого автоматически следует приятный бонус: кавычки внутри строки тоже обычно можно писать без \" (потому что raw‑строка закрывается не кавычкой внутри, а специальной последовательностью).

Базовый синтаксис выглядит так:

R"(текст)"

Внутри круглых скобок можно писать почти любой текст, включая кавычки и обратные слеши. Важно, что raw‑строки — это часть языка C++, а не библиотека: никакие заголовки не нужны. В стандарте это именно raw string literal — отдельный вид строкового литерала.

Сравним обычную строку и raw‑строку на примере пути:

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

Обе строки дают одинаковый результат, но raw‑вариант читается как нормальный человеческий путь, а не как тест на внимательность.

3. Нюансы raw‑строк в реальном коде

\n — это не перенос, пока вы не сделали перенос

Здесь обычно случается первая “подстава для новичка”: raw‑строка не “понимает” escape‑последовательности, а значит \n внутри неё — это два символа \ и n. Если вы хотите реальный перевод строки, вы должны… реально нажать Enter в тексте raw‑строки. Это звучит слишком просто, но из-за привычки к "\n" многие получают неожиданный вывод.

Покажем это максимально честно:

#include <iostream>
#include <string>

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

    std::cout << a << '\n'; // A (перенос строки) B
    std::cout << b << '\n'; // A\nB
}

И вот тут важно не перепутать цели. Когда вы пишете “данные в исходнике” и хотите, чтобы там были символы \ и n (например, вы показываете пользователю пример команды), raw‑строка идеальна. Когда вы хотите реально вставить управляющий символ табуляции или перевода строки внутри одной строки, обычный литерал или конкатенация с '\n' часто удобнее.

Многострочные raw‑строки: справка и “встроенные данные”

Если вы когда-нибудь писали help‑сообщение для консольной программы, вы знаете: сначала всё красиво, а потом добавляются примеры с кавычками и обратными слешами — и начинается “праздник экранирования”. Raw‑строка позволяет хранить такой текст в исходнике “как в документации”, с нормальными переносами строк.

Представим, что мы продолжаем наше учебное CLI‑приложение “Список покупок”. В предыдущих лекциях мы уже умели парсить команду вида add "milk chocolate" 3 через std::istringstream и std::quoted. Теперь хотим добавить команду help, которая печатает инструкции.

#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; // печатает многострочный текст
}

Текст выглядит “как человек написал”, а не как “компилятор заставил”.

А теперь второй полезный сценарий: вы хотите встроить в код небольшой набор тестовых данных. Например, “мини‑скрипт”, который имитирует ввод пользователя, чтобы вы могли быстро прогнать парсер. Это особенно удобно, когда вы объясняете тему или проверяете логику без ручного ввода.

Схема получается такая:

flowchart TD
    A["raw-строка: сценарий ввода"] --> B["std::istringstream"]
    B --> C["std::getline: читаем построчно"]
    C --> D["parseCommand(line): парсим одну команду"]
    D --> E["действие: add/list/help"]

И код “скрипта” можно хранить прямо в raw‑строке:

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

Заметьте, мы не обсуждаем файлы и не строим “настоящую” систему тестов — это будет позже в курсе. Сейчас это просто удобный способ не мучить себя ручным вводом, когда вы отлаживаете парсинг строк.

Пользовательский delimiter: R"TAG( ... )TAG"

У raw‑строки есть одно ограничение, о котором полезно знать заранее, чтобы потом не спорить с компилятором на повышенных тонах. Базовая raw‑строка закрывается последовательностью )". Значит, если внутри текста случайно встретится )", компилятор может решить, что строка закончилась раньше, и дальше начнётся “парад ошибок”.

Чтобы решить эту проблему, C++ позволяет задавать свой “тэг” (delimiter). Тогда строка открывается как R"TAG( и закрывается как )TAG". И вы выбираете TAG так, чтобы он точно не встречался внутри текста.

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

Это выглядит необычно, но работает очень логично: “открыли с TAG — закрыли с TAG”. Именно такой вариант спасает, когда вы храните в строке кусок кода, JSON, или текст, где )" вполне может встретиться.

Поскольку raw‑строки — это часть лексики языка (на уровне “как компилятор читает токены”), в стандарте C++ к ним относятся довольно серьёзно и отдельно уточняют детали вроде корректного распознавания символа R у raw‑литералов и ссылок на эту тему в разделе лексики.

Raw‑строки и std::quoted: разные уровни задачи

Очень частая путаница у новичков звучит так: “Я же уже знаю std::quoted, зачем мне raw‑строки?” А потом возникает вторая: “Я написал raw‑строку, значит std::quoted не нужен?” И обе идеи неправильные, просто потому что это инструменты для разных задач.

Raw‑строка — это про то, как вы задаёте строку в исходнике C++. Это удобно для литералов, справки, тестовых данных. std::quoted — это про то, как вы читаете и пишете строки в формате данных, когда по контракту строка берётся в кавычки, чтобы в ней могли быть пробелы. Эти два уровня легко перепутать, потому что и там, и там фигурируют кавычки.

Небольшая таблица, чтобы закрепить различие:

Вопрос Raw‑строка R"( ... )" std::quoted
Где используется? В исходнике C++ (литерал) В потоковом вводе/выводе данных
Что решает? Убирает экранирование \\, \", сохраняет переносы Читает/пишет строку в кавычках как один токен
“Понимает” ли \n? Нет, это два символа \ и n Это зависит от данных, std::quoted работает с экранированием в формате
Нужен ли заголовок? Нет (языковая конструкция) Да, <iomanip>

И вот пример, где они дружат: raw‑строка позволяет удобно записать тестовую строку, а std::quotedправильно её распарсить.

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

Raw‑строка тут просто делает тест читаемым. Парсинг делает std::quoted.

4. Встраиваем raw‑строки в CLI: help и demo‑скрипт

Сейчас мы сделаем маленький, но очень практичный шаг: добавим к нашему приложению две вещи — многострочную справку и “демо‑ввод” для отладки. Обратите внимание, что мы не прыгаем в архитектуру, файлы, классы и прочие большие темы. Мы остаёмся в рамках сегодняшнего дня: строки, потоки, getline, quoted.

Начнём с функции, которая возвращает help‑текст. В реальном проекте вы бы вынесли это аккуратнее, но сейчас наша цель — тренировка raw‑строк.

#include <string>

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

Теперь в main можно печатать справку по команде help. Упростим: пока просто сравним строку целиком (мы уже умеем if/else и getline).

#include <iostream>
#include <string>

std::string BuildHelpText();

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

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

Следующий шаг — демо‑скрипт. Представим, что мы хотим “прогнать” несколько команд без ручного ввода. Мы сделаем переменную demoInput, которая является std::istringstream, созданным из raw‑строки. Это очень удобно на занятиях: один код — и у всех студентов одинаковый сценарий.

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

И вот как можно читать команды построчно из такого потока:

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

Здесь есть тонкий момент, который на практике улучшает жизнь: raw‑строка сохраняет переводы строк в точности, и вам не надо писать "...\n...\n" и следить за кавычками. Получается “мини‑файл”, встроенный прямо в исходник, и вы его читаете так же, как читали бы обычный ввод.

Если дальше вы подключите ваш парсер команды (из прошлых лекций, где был std::istringstream + std::quoted), то этот demo‑скрипт станет идеальным способом быстро проверять, что парсинг не сломался после изменений.

5. Типичные ошибки при работе с raw‑строками

Ошибка №1: ожидание, что \n внутри raw‑строки станет переводом строки.
В обычных строках "\n" — это специальный символ переноса строки, и мозг быстро привыкает к этому. В raw‑строке R"(\n)" — это буквально два символа, обратный слеш и буква n. Если нужен реальный перенос, делайте реальный перенос строк в тексте raw‑литерала.

Ошибка №2: “почему компилятор ругается на середине строки?” — случайный )" внутри текста.
Raw‑строка закрывается последовательностью )", и если ваш текст содержит её (например, кусок кода или данных), компилятор решит, что литерал закончился, а дальше начнётся хаос из “неожиданных токенов”. Лечится это пользовательским delimiter’ом: R"TAG( ... )TAG".

Ошибка №3: путаница уровней — raw‑строка вместо std::quoted или наоборот.
Raw‑строки решают проблему записи в исходнике, а не проблему чтения из входных данных. Если пользователь вводит add milk chocolate 3, raw‑строка никак не поможет прочитать “milk chocolate” как один токен — это задача std::quoted и контракта формата (“строки с пробелами берём в кавычки”). И наоборот, std::quoted не делает ваш исходник читабельнее, если вы пытаетесь в коде написать текст с C:\temp\....

Ошибка №4: выбор delimiter’а, который встречается внутри текста.
Иногда студенты ставят TAG, а потом копируют в raw‑строку текст, где встречается )TAG". Итог такой же, как и без delimiter’а: строка “внезапно закрылась”. Правило простое: выбирайте delimiter, который точно уникален для данного блока (хоть R"__HELP__( ... )__HELP__"), и не ленитесь сделать его странным — это как пароль: лучше пусть будет некрасивый, зато надёжный.

Ошибка №5: попытка использовать raw‑строку там, где нужны управляющие символы.
Raw‑строки великолепны для “текста как есть”, но если вам нужно именно сформировать строку с табуляциями, переводами строки, звуковым сигналом (да, такое бывает), то обычный литерал с \t, \n может быть проще. Raw‑строка — инструмент, а не религия: иногда слеши полезны.

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