JavaRush /Курси /C++ SELF /Raw‑рядки R"( ... )" у C++

Raw‑рядки R"( ... )" у C++

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

Наступний крок — демо‑скрипт. Уявімо, що ми хочемо «прогнати» кілька команд без ручного введення. Ми побудуємо джерело введення на основі 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), то цей демо‑скрипт стане ідеальним способом швидко перевіряти, що розбір не зламався після змін.

5. Типові помилки під час роботи з raw‑рядками

Помилка № 1: очікування, що \n усередині raw‑рядка стане перенесенням рядка.
У звичайних рядках "\n" — це спеціальний символ перенесення рядка, і мозок швидко звикає до цього. У raw‑рядку R"(\n)" — це буквально два символи, зворотний слеш і літера n. Якщо потрібне реальне перенесення, робіть реальне перенесення рядків у тексті raw‑літерала.

Помилка № 2: «чому компілятор свариться посередині рядка?» — випадкове )" усередині тексту.
Raw‑рядок закривається послідовністю )", і якщо ваш текст містить її (наприклад, фрагмент коду або даних), компілятор вирішить, що літерал закінчився, а далі почнеться хаос із «неочікуваних токенів». Розвʼязання просте: використовуйте користувацький роздільник R"TAG( ... )TAG".

Помилка № 3: плутанина рівнів — raw‑рядок замість std::quoted або навпаки.
Raw‑рядки розвʼязують проблему запису у вихідному коді, а не проблему читання з вхідних даних. Якщо користувач вводить add milk chocolate 3, raw‑рядок ніяк не допоможе прочитати «milk chocolate» як один токен — це задача std::quoted і контракту формату («рядки з пробілами беремо в лапки»). І навпаки, std::quoted не зробить ваш вихідний код читабельнішим, якщо ви намагаєтеся записати в коді текст із C:\temp\....

Помилка № 4: вибір роздільника, який трапляється всередині тексту.
Іноді студенти ставлять TAG, а потім копіюють у raw‑рядок текст, де трапляється )TAG". Результат такий самий, як і без роздільника: рядок «несподівано закрився». Правило просте: обирайте роздільник, який точно унікальний для цього блока (хоч R"__HELP__( ... )__HELP__"), і не лінуйтеся зробити його дивним — це як пароль: краще нехай буде негарний, зате надійний.

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

1
Опитування
Парсинг рядків, рівень 17, лекція 4
Недоступний
Парсинг рядків
Парсинг рядків
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ