DSL на C++: как создать свой язык программирования для реальных задач

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Когда C++ — это не просто язык, а конструктор языков

Представьте, что вы пишете сложную систему управления роботом-манипулятором. Каждый день вы вбиваете однотипные команды: «повернуть сервопривод на 30 градусов», «захватить деталь», «переместить в точку X:100 Y:200». Код на чистом C++ превращается в бесконечную простыню вызовов функций и циклов. А что если бы вы могли написать команду прямо как на человеческом языке: move arm to (100, 200) at speed 50? И чтобы это был настоящий, компилируемый C++ код?

Это не фантастика. Это — DSL (Domain-Specific Language), или предметно-ориентированный язык. На C++ вы можете создать свой мини-язык, который будет идеально описывать задачи вашей предметной области. В этой статье мы не просто поговорим о теории — мы напишем настоящий DSL на C++ с нуля, разберём два подхода (внутренний и внешний) и покажем, как это ускорит разработку в 5-10 раз.

Добро пожаловать в мир, где программист становится лингвистом, а компилятор — вашим переводчиком.

Что такое DSL и зачем он вам нужен?

Предметно-ориентированный язык (DSL) — это язык программирования, созданный для решения задач одной конкретной области. В отличие от общего языка (C++, Python, Java), DSL очень узко специализирован. Примеры, которые вы наверняка знаете: SQL (для баз данных), Regex (для поиска паттернов), HTML (для разметки веб-страниц).

Но зачем писать свой DSL на C++? Три главные причины:

  • Читаемость кода. Ваш код становится похож на техническое задание. Его может читать и менять не только программист, но и инженер, математик, дизайнер.
  • Скорость разработки. Вместо 20 строк шаблонного кода — одна строка на DSL.
  • Безопасность. DSL не даст написать синтаксически неверную команду для вашей системы. Ошибки отлавливаются на этапе компиляции.

C++ идеален для создания DSL благодаря мощной системе шаблонов (template metaprogramming), операторам перегрузки и макросам. Вы можете сделать DSL, который будет выполняться так же быстро, как и обычный C++ код, потому что он будет встроен прямо в компилятор.

Два мира: Internal DSL vs External DSL

Прежде чем писать код, давайте выберем подход. Существует два типа DSL для C++:

Internal DSL (встроенный DSL)

Вы не пишете новый парсер. Вы используете синтаксис C++ — перегрузку операторов, цепочки вызовов, лямбды — чтобы создать «язык внутри языка». Это самый популярный подход в C++. Пример: библиотека Boost.Spirit или Eigen для линейной алгебры, где вы пишете v = (3.0 * m1 + m2) * m3; — это выглядит как математический DSL, но это чистый C++.

External DSL (внешний DSL)

Вы придумываете свой синтаксис с нуля, пишете парсер (или используете генератор парсеров типа ANTLR, Flex/Bison), а потом транслируете его в C++ код. Это сложнее, но даёт полную свободу. Например, вы можете написать скрипт на своём языке, а программа на C++ его выполнит.

В этой статье мы сделаем оба варианта. Начнём с простого — внутреннего DSL.

Практика 1: Создаём Internal DSL для конфигурации робота

Допустим, мы пишем библиотеку для управления роботом. Нам нужно задавать последовательность действий. Обычный код выглядел бы так:

Robot robot;
robot.connect("COM3");
robot.setSpeed(50);
robot.moveTo(100, 200);
robot.grip(true);
robot.moveTo(300, 400);
robot.grip(false);

Работает, но скучно. Давайте превратим это в DSL, используя цепочки методов и перегрузку операторов. Мы хотим писать так:

Robot robot;
robot << connect("COM3") << speed(50)
      << move(100, 200) << grip(on)
      << move(300, 400) << grip(off);

Как это реализовать? Используем шаблон «Builder» и перегрузку оператора <<.

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

// Вспомогательные структуры для команд
struct Command {
    virtual void execute() = 0;
    virtual ~Command() = default;
};

struct ConnectCmd : Command {
    std::string port;
    void execute() override { std::cout << "Connecting to " << port << "\n"; }
};

struct MoveCmd : Command {
    int x, y;
    void execute() override { std::cout << "Moving to (" << x << ", " << y << ")\n"; }
};

struct SpeedCmd : Command {
    int speed;
    void execute() override { std::cout << "Set speed to " << speed << "\n"; }
};

struct GripCmd : Command {
    bool state;
    void execute() override { 
        std::cout << (state ? "Grip ON" : "Grip OFF") << "\n"; 
    }
};

// Функции для создания команд (они возвращают unique_ptr)
auto connect(const std::string& p) { return std::make_unique<ConnectCmd>(p); }
auto move(int x, int y) { return std::make_unique<MoveCmd>(x, y); }
auto speed(int s) { return std::make_unique<SpeedCmd>(s); }
auto grip(bool s) { return std::make_unique<GripCmd>(s); }

// Класс Robot с перегруженным оператором <<
class Robot {
    std::vector<std::unique_ptr<Command>> commands;
public:
    Robot& operator<<(std::unique_ptr<Command> cmd) {
        commands.push_back(std::move(cmd));
        return *this;
    }
    void run() {
        for (auto& cmd : commands) cmd->execute();
    }
};

int main() {
    Robot robot;
    robot << connect("COM3") << speed(50)
          << move(100, 200) << grip(true)
          << move(300, 400) << grip(false);
    robot.run();
    return 0;
}

Этот код — классический Internal DSL на C++. Он чистый, типизированный, а главное — компилятор проверяет всё на этапе сборки. Вы не сможете написать grip(123) — только true или false.

Практика 2: External DSL с помощью макросов (препроцессор)

Иногда хочется ещё более «человеческого» синтаксиса. Тут на помощь приходят макросы C++. Да, это «зло», но в умелых руках — мощный инструмент для создания DSL.

Предположим, мы хотим писать так:

ROB                

Блоги

Рекомендаций по книгам