Как стать автором
Поиск
Написать публикацию
Обновить

Ошибки, которые не случились: C++ и compile‑time проверка SQL-запросов

Уровень сложностиСредний
Время на прочтение15 мин
Количество просмотров3.6K

Предыстория

Ключевые слова constexpr/consteval в С++ живут уже не первый год, но для многих по‑прежнему остаются чем‑то неясными или чересчур академичными. По старой памяти их роль ограничивается чем‑то вроде «вычислить факториал в compile‑time», «сделать генератор чисел» или, в лучшем случае, «записать пару if constexpr для метапрограммирования». Словом, игрушка для шаблонных фокусов, но не инструмент, способный менять архитектурную парадигму.

Однако C++ постепенно эволюционирует. С выходом новых стандартов (C++20, C++23 и предстоящего C++26) и с расширением constexpr‑совместимости в стандартной библиотеке, — пространство применения constexpr и consteval стремительно выходит за рамки тривиальных вычислений. Мы получаем возможность работать с полноценными структурами данных, парсить текст, реагировать на ошибки осмысленно и строго уже в момент компиляции, а не где‑то в CI или, что хуже, в runtime. Именно здесь возникает новое мышление: если что‑то может быть проверено до запуска — оно обязано быть проверено до запуска.

В этой статье мы посмотрим, как можно реализовать полную compile‑time валидацию SQL‑запросов на основе схемы базы данных, встраиваемой прямо в код. Без магии, без рантайма, без сторонних тулов. Только стандартный C++ и ваша структура БД. Валидация таблиц, столбцов, типов аргументов и их количества — всё на compile‑time.

Представьте, если бы компилятор сам указывал «такой таблицы нет», «несуществующий столбец», «несовместимые типы» — до запуска программы. Такой подход полностью устраняет «сюрпризы» во время исполнения и исключает класс ошибок, связанных с генерацией SQL во время работы программы. Ваша программа даже не соберётся.

Ссылка на полный проект с тестовыми примерами


Архитектура решения

Архитектура механизма проверки SQL на этапе компиляции строится на следующих шагах:

  1. Схема базы данных. Описываем структуру БД в статическом файле (например, XML, JSON или SQL DDL) с таблицами, полями и их типами.

  2. Встраивание схемы. Содержимое схемы включается в исходники. В будущем C++26 для этого может использоваться директива #embed, которая автоматически встраивает файл как массив байт:

    constexpr const char data[] = { #embed "test.json", 0};

    В C++20/23 можно использовать, например, скрипт‑генератор и преобразовать содержимое файла в строковый литералconstexpr std::string_view.

  3. Парсинг схемы на этапе компиляции. Специальная consteval ‑функция (или constexpr ‑функция, выполняемая при компиляции) читает string_view схемы и анализирует ее. В результате компиляции формируется константная модель БД: таблицы преобразуются в constexpr структуры или массивы, столбцы — в constexpr значения. Получаем в коде C++ типобезопасное описание схемы (наподобие «отображения» из имен в типы полей).

  4. SQL‑валидатор на этапе компиляции. Любой SQL‑запрос в коде оформляется, например, как строковый литерал с передачей аргументов полей. С помощью consteval ‑функций мы разбираем этот запрос на лексемы (SELECT, FROM, WHERE и т. д.), сверяем упоминаемые таблицы и поля с ранее построенной схемой. Если находится несоответствие (не та таблица, не тот столбец, несовместимые типы), генерируем static_assert с подробным сообщением. Если проверка проходит, компиляция продолжается.

Благодаря таким метаданным мы имеем все имена таблиц/столбцов и их типы. Эти структуры определяются в коде (сгенерированы из внешнего описания) и имеют только constexpr‑данные. В схеме нет дублей: исходная информация хранится только в одном месте (схема базы данных) и преобразуется в C++‑типы автоматически. Таким образом, уже к моменту валидации запросов компилятор «знает», какие таблицы существуют и какие поля в них есть.

Схема описывается несколькими простыми структурами:

Column — описывает столбец: содержит имя ( std::string_view ) и тип поля.

Table — описывает таблицу: содержит имя таблицы и массив std::array<Column> полей.

Schema — список всех таблиц в базе.


constexpr size_t MAX_COLUMNS = 100;
constexpr size_t MAX_TABLES = 100;

struct Table
{
   enum ColumnType : int
   {
      BOOLEAN,
      DATETIME,
      DOUBLE,
      ENUM,
      HASHTABLE,
      IDENTIFIER,
      INT64,
      INTEGER,
      MONEY,
      STRING,
      UUID,
   };
   struct Column
   {

      std::string_view mName;
      ColumnType mType;
      bool mIsNotNull = false;
   };

   std::string_view mName;
   std::array< Column, MAX_COLUMNS > mColumns{};
   size_t Column_count = 0;
};

struct Schema
{
   std::array< Table, MAX_TABLES > mTables{};
   size_t mTableCount = 0;
};

Здесь вынужден сделать уточнение, что столько кривой вариант работы через std::array — вынужденная необходимость С++23, которая уже в С++26 позволит оперировать векторами и иными контейнерами с динамической памятью.

Для того, чтобы перетащить актуальную версию схемы базы данных (для примера это будет XML‑файл, описывающий схему базы данных в формате, характерном для внутреннего DSL), я набросал небольшую кастомную команду в cmake:

add_custom_command(
  OUTPUT ${CMAKE_BINARY_DIR}/generated_dicx.hpp
  COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}
  COMMAND python3 ${CMAKE_SOURCE_DIR}/tools/embed_dicx.py
          ${CMAKE_SOURCE_DIR}/db.dicx
          ${CMAKE_BINARY_DIR}/generated_dicx.hpp
  DEPENDS ${CMAKE_SOURCE_DIR}/db.dicx
)

Которая запускает python‑скрипт по копированию файла схемы в хэдер‑файлик, который затем будет подключен к проекту для парсинга в нашу структуру. Суть скрипта — предоставить нам при компиляции актуальный вид БД в constexpr виде:

constexpr std::string_view dicx_xml = 
  R"DICX(<?xml version="1.0" encoding="UTF-8" ?>
    <table name="Company" responsible="Косинцев А.В.">
      <comment>Таблица для компаний</comment>
      <column index_in_pk="0" is_pk_part="1" name="Uuid">
        <comment>Uuid компании</comment>
        <format>
          <type>UUID</type>
          <not_null>true</not_null>
        </format>
      </column>
    </table>
  <!-- другие столбцы и таблицы... -->
)DICX";

В C++26 это будет решено директивой #embed, без внешних скриптов, но для поддержания работоспособности примера в С++23 сойдет и текущий способ.

Это небольшое достижение позволит нам перейти к парсингу dicx_xml.

Для предоставления общего интерфейса парсера используется паттерн CRTP (Curiously Recurring Template Pattern). Так, наш шаблонный класс BaseParserCRTP — это наиболее простой способ заиметь constexpr-поле класса, инициализируемое наследником, который сам определит, как распарсить свою схему базы данных. Сам же BaseParserCRTP реализует общую логику получения готовой структуры Schema, ее таблиц и их количества.

template< typename Derived >
class BaseParserCRTP
{
protected:
   static inline constexpr Schema mSchema = Derived::Parse();

public:
   consteval static size_t TableCount()
   {
      return mSchema.mTableCount;
   }

   consteval static const Table* GetTable( std::string_view name )
   {
      for( auto const& t : mSchema.mTables )
         if( t.mName == name )
            return &t;
      return nullptr;
   }
};

Для своего XML‑варианта я определю своего наследника с нужным парсером, и именно он получит на обработку нашу сгенерированную версию схемы БД:

#include "base_parser.hpp"
#include "generated_dicx.hpp"
#include <string_view>

class DicxParser : public BaseParserCRTP< DicxParser >
{
public:
   consteval static Schema Parse();

private:
   using ColumnType = Table::ColumnType;

   static consteval size_t FindTag( std::string_view xml, std::string_view tag, size_t start = 0 );

   static consteval std::string_view ExtractAttr( std::string_view tag, std::string_view key );

   static consteval ColumnType ParseColumnType( std::string_view s );
};

inline consteval Schema DicxParser::Parse()
{
   Schema r;
   size_t pos = 0;
   while( true )
   {
      auto tbl_pos = FindTag( dicx_xml, "<table ", pos );
      if( tbl_pos == dicx_xml.size() )
         break;

      auto end_tag = FindTag( dicx_xml, ">", tbl_pos );
      auto tag_str = dicx_xml.substr( tbl_pos, end_tag - tbl_pos );
      auto name = ExtractAttr( tag_str, "name=" );
      auto& table = r.mTables[ r.mTableCount++ ];
      table.mName = name;

      size_t col_pos = end_tag;
      auto table_close = FindTag( dicx_xml, "</table>", end_tag );
      while( true )
      {
         auto cpos = FindTag( dicx_xml, "<column ", col_pos );
         if( cpos >= table_close )
            break;

         auto cend = FindTag( dicx_xml, ">", cpos );
         auto ctag = dicx_xml.substr( cpos, cend - cpos );
         auto col_name = ExtractAttr( ctag, "name=" );

         auto& Column = table.mColumns[ table.Column_count++ ];
         Column.mName = col_name;

         auto tpos = FindTag( dicx_xml, "<type>", cend );
         auto tend = FindTag( dicx_xml, "</type>", tpos );
         auto type_name = utils::Trim( dicx_xml.substr( tpos + 6, tend - tpos - 6 ) );
         Column.mType = ParseColumnType( type_name );

         auto nn_pos = FindTag( dicx_xml, "<not_null>", tpos );
         if( nn_pos < table_close && nn_pos < FindTag( dicx_xml, "</not_null>", nn_pos ) )
         {
            auto nn_end = FindTag( dicx_xml, "</not_null>", nn_pos );
            Column.mIsNotNull = dicx_xml.substr( nn_pos + 10, nn_end - nn_pos - 10 ) == "true";
         }

         col_pos = tend;
      }

      pos = table_close;
   }
   return r;
}

inline consteval DicxParser::ColumnType DicxParser::ParseColumnType( std::string_view s )
{
   if( s == "UUID" )
      return ColumnType::UUID;
   if( s == "STRING" )
      return ColumnType::STRING;
   if( s == "INTEGER" )
      return ColumnType::INTEGER;
   if( s == "DOUBLE" )
      return ColumnType::DOUBLE;
   if( s == "BOOLEAN" )
      return ColumnType::BOOLEAN;
   if( s == "DATETIME" )
      return ColumnType::DATETIME;
   if( s == "ENUM" )
      return ColumnType::ENUM;
   if( s == "HASHTABLE" )
      return ColumnType::HASHTABLE;
   if( s == "IDENTIFIER" )
      return ColumnType::IDENTIFIER;
   if( s == "INT64" )
      return ColumnType::INT64;
   if( s == "MONEY" )
      return ColumnType::MONEY;

   throw "Unknown Column type: " __FILE__;
}

DicxParser наследуется от BaseParserCRTP и знает формат нашего.dicx XML (таблицы, столбцы, типы). За счёт CRTP весь парсинг происходит без виртуальных вызовов.

После того как схема представлена в constexpr‑структурах, любой SQL‑запрос можно проверить компилятором. Это может быть простой рекурсивный разбор (например, только SELECT‑FROM‑WHERE без сложных JOIN), выполненный в consteval функции.

При этом собственным критерием удобства был формат написания запросов, по типу таких:

SQL::Select< "SELECT Folder, Id FROM CompanyFolderCounters" >();
SQL::Insert< "INSERT INTO Company (Description, Uuid, Name, Rating)" >
           ( description, uuid, name, rate );

Это позволило бы оставить гибкость SQL‑языка, и при этом не потерять аргументы полей.

Наш SqlChecker крайне прост и его методы параметризуются ровно тем способом, пример использования которого вы видите выше. Разумеется, представленный вариант крайне примитивен и служит целью показать возможность использования С++ для валидации SQL‑запросов.

template< typename T >
concept DBParser = std::derived_from< T, BaseParserCRTP< T > >;

template< DBParser Parser >
class SqlChecker
{
public:
   virtual ~SqlChecker() = default;
   consteval SqlChecker() = default;

   consteval static inline size_t TableCount();

   template< FixedString SQL, typename... Args >
   consteval void CheckInsert() const;

   template< FixedString SQL >
   consteval void ValidateSelect() const;
private:
   consteval static Table const* GetTable( std::string_view table_name )
   {
      return Parser::GetTable( table_name );
   }
};

Из‑за ограничений С++23 на constexpr‑контейнеры, пришлось, не дожидаясь 26 года, сообразить FixedString, который бы не содержал ничего лишнего, и полностью умещался в constexpr контекст:

template< size_t N >
struct FixedString
{
   char mData[ N ]{};

   constexpr FixedString( const char ( &str )[ N ] )
   {
      for( size_t i = 0; i < N; ++i )
         mData[ i ] = str[ i ];
   }

   constexpr operator std::string_view() const
   {
      return { mData, N - 1 };
   }
};

Иначе вы бы сразу столкнулись с сообщением вроде:

“Type 'std::string_view' is not a structural type because it has a non‑static data member that is not public.”

Мой простенький метод валидации вычитки из базы данных выглядит так:

template< FixedString SQL >
   consteval void ValidateSelect() const
   {
      constexpr std::string_view sql = SQL;
      if constexpr( constexpr bool is_select = sql.starts_with( "SELECT" ); !is_select )
         static_assert( is_select, "It's not SELECT query" );

      constexpr auto from = sql.find( "FROM" );
      if( constexpr bool is_from = from != std::string_view::npos; !is_from )
         static_assert( is_from, "FROM not found" );

      constexpr auto columns = sql.substr( 7, from - 7 );

      if constexpr( utils::Trim( columns ) == "*" )
      {
         constexpr auto tbl = utils::Trim( sql.substr( from + 5 ) );
         static_assert( GetTable( tbl ), "Unknown table" );
      }
      else
      {
         constexpr auto tbl = utils::Trim( sql.substr( from + 5 ) );
         constexpr auto sd = GetTable( utils::Trim( tbl ) );

         if constexpr( constexpr bool is_table_present = sd; !is_table_present )
            static_assert( is_table_present, "Unknown table" );

         constexpr auto column_result = utils::Split( columns, ',' );
         constexpr auto column_tokens = column_result.first;
         constexpr size_t column_count = column_result.second;
         // Проверка одного аргумента по соответствию в БД
         auto test_one = [ & ]< size_t I >() consteval
         {
            constexpr auto name = utils::Trim( column_tokens[ I ] );

            constexpr bool ok = [ & ]
            {
               for( size_t j = 0; j < sd->Column_count; ++j )
                  if( sd->mColumns[ j ].mName == name )
                     return true;
               return false;
            }();

            if constexpr( !ok )
            {
               static_assert( ok, "Unknown column" );
            }
         };

         [ & ]< size_t... Is >( std::index_sequence< Is... > ) consteval
         { ( test_one.template operator()< Is >(), ... ); }( std::make_index_sequence< column_count >{} );
      }
   }

Здесь целая эпопея из слова constexpr, но таковы требования языка к тому, чтобы убедиться в compile‑time принадлежности той или иной переменной. Отдельная боль для column_result в том, что декомпозиция пока что не может быть constexpr.

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

Ради справедливости приложу версию Insert валидации, но полную версию всего здесь описанного можно будет глянуть и потыкать ЗДЕСЬ:

template< FixedString SQL, typename... Args >
   consteval void CheckInsert() const
   {
      constexpr std::string_view sqlv = SQL;

      // Проверка, что запрос начинается с INSERT
      if constexpr( !sqlv.starts_with( "INSERT" ) )
      {
         static_assert( false, "It's not INSERT query" );
      }

      // Нахождение позиции "INTO" и скобок
      constexpr auto into_pos = sqlv.find( "INTO" );
      if constexpr( into_pos == std::string_view::npos )
      {
         static_assert( false, "INTO not found in INSERT query" );
      }
      else
      {

         // Извлечение списка полей между скобками
         constexpr auto paren1 = sqlv.find( '(', into_pos );
         constexpr std::string_view table = utils::Trim( sqlv.substr( into_pos + 4, paren1 - ( into_pos + 4 ) ) );
         constexpr auto paren2 = sqlv.find( ')', paren1 );

         constexpr std::string_view Columns_list = sqlv.substr( paren1 + 1, paren2 - ( paren1 + 1 ) );

         constexpr auto split_result = utils::Split( Columns_list, ',' );
         constexpr auto columns = split_result.first;
         constexpr size_t fcount = split_result.second;

         // Получаем дескриптор таблицы по имени
         constexpr auto sd = GetTable( table );
         if constexpr( !sd )
         {
            static_assert( false, "Unknown table" );
         }

         // Проверка, что количество аргументов совпадает с количеством полей
         if constexpr( fcount != sizeof...( Args ) )
         {
            static_assert( false, "Column count is not equal to argument count" );
         }
         else
         {
            constexpr auto trimmed_Columns = [ & ]() consteval
            {
               std::array< std::string_view, sizeof...( Args ) > out{};
               for( size_t i = 0; i < sizeof...( Args ); ++i )
               {
                  out[ i ] = utils::Trim( columns[ i ] );
               }
               return out;
            }();
            // Проверка одного аргумента по соответствию в БД
            auto test_one = [ & ]< typename T, size_t I >() consteval
            {
               constexpr std::string_view column_name = trimmed_Columns[ I ];

               // 1) Сначала проверяем, существует ли такое поле в sd->Columns
               constexpr bool column_exists = [ & ]() consteval -> bool
               {
                  for( size_t j = 0; j < sd->Column_count; ++j )
                  {
                     if( sd->mColumns[ j ].mName == column_name )
                     {
                        return true;
                     }
                  }
                  return false;
               }();

               if constexpr( !column_exists )
               {
                  static_assert( false, "Unknown column name" );
               }
               else
               {
                  // 2) Теперь безопасно можем найди индекс поля (поскольку он точно есть)
                  constexpr size_t found_idx = [ & ]() consteval
                  {
                     for( size_t j = 0; j < sd->Column_count; ++j )
                     {
                        if( sd->mColumns[ j ].mName == column_name )
                        {
                           return j;
                        }
                     }
                     return size_t( 42 );
                  }();

                  // 3) Проверяем тип T на совпадение с типом поля
                  using U = std::decay_t< T >;
                  constexpr auto f_type = sd->mColumns[ found_idx ].mType;
                  if constexpr( f_type == Table::INTEGER )
                  {
                     static_assert( std::is_same_v< U, int >, "Type mismatch: expected INTEGER" );
                  }
                  else if constexpr( f_type == Table::DOUBLE )
                  {
                     static_assert( std::is_same_v< U, double >, "Type mismatch: expected DOUBLE" );
                  }
                  else if constexpr( f_type == Table::STRING )
                  {
                     static_assert( std::is_same_v< U, std::string >, "Type mismatch: expected STRING" );
                  }
                  else if constexpr( f_type == Table::UUID )
                  {
                     static_assert( std::is_same_v< U, Uuid >, "Type mismatch: expected UUID" );
                  }
                  else if constexpr( constexpr bool is_Column_not_found = true )
                  {
                     static_assert( false && sd->mColumns[ found_idx ].mType,
                                    "Type mismatch: unknown column type in table" );
                  }
               }
            };

            [ & ]< size_t... Is >( std::index_sequence< Is... > ) consteval
            {
               ( test_one.template operator()< typename std::tuple_element< Is, std::tuple< Args... > >::type, Is >(),
                 ... );
            }( std::make_index_sequence< fcount >{} );
         }
      }
   }

В отличие от SELECT, валидация INSERT требует строгой проверки количества и порядка значений — в точном соответствии с порядком полей, а также передаваемых типов аргументов - Uuid - значит - Uuid.

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

Однако, цель достигнута и работа нашего SqlChecker'a примерно такая:

1) Лексический анализ: строковый литерал запроса разбивается на токены (ключевые слова, имена таблиц/полей, литералы, операторы).

2) Проверка синтаксиса: анализируем структуру запросов (простая грамматика SELECT). Если синтаксис не соответствует поддерживаемому подмножеству SQL, вызываем static_assert.

3) Проверка таблиц/столбцов: для каждого упомянутого имени таблицы ищем соответствующую таблицу в constexpr‑схеме. Аналогично проверяем поля и их типы: например, убеждаемся, что литерал конвертируется в тип поля (например, не сравниваем число с текстом). При несовместимости типов — static_assert(«Type mismatch:...»).

Примитивный валидатор для SQL‑запросов готов, и теперь осталось его красиво обернуть:

#include "dicx_parser.hpp"
#include "sql_checker.hpp"

/// Обертка для работы с БД
struct SQL
{
   template< FixedString Query, typename... Args >
   constexpr static void Insert( Args const&... args )
   {
      static_assert( mChecker.TableCount() > 0, "No tables loaded" );
      mChecker.CheckInsert< Query, Args... >();

      // Здесь обычная вставка
      // Вызывается только если compile-time проверка прошла успешно
   }

   template< FixedString Query >
   consteval static void Select()
   {
      static_assert( mChecker.TableCount() > 0, "No tables loaded" );
      mChecker.ValidateSelect< Query >();

      // Здесь обычная вставка
      // Вызывается только если compile-time проверка прошла успешно
   }

private:
   inline static constexpr SqlChecker< DicxParser > mChecker;
};

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


Ошибки, отловленные во время компиляции

Uuid uuid;
std::string description = "description", name = "name";
double rate = 4.6;
int int_uuid;

// Случайно передадим вместо Uuid-идентификатора - простое целое число
SQL::Insert< "INSERT INTO Company (Description, Uuid, Name, Rating)" >
           ( description, int_uuid, name, rate );
// ВЫВОД: error: In template: static assertion failed due to requirement 
// 'std::is_same_v<int, Uuid>': Type mismatch: expected UUID


// Передадим меньшее количество аргументов, чем полей на вставку
SQL::Insert< "INSERT INTO Company (Description, Uuid, Name, Rating)" >
           ( description, name, rate );
// ВЫВОД: error: In template: static assertion failed: 
// Column count is not equal to argument count

// Случайно ошибемся в названии столбца Uuid
SQL::Insert< "INSERT INTO Company (Description, UUid, Name, Rating)" >
           ( description, uuid, name, rate );
// ВЫВОД: error: In template: static assertion failed: Unknown column name

// Если такой таблицы нет, то:
SQL::Select< "SELECT Id FROM Car" >();
// ВЫВОД: Unknown table

И, разумеется, где-то я лукавлю, поскольку иногда сообщения могут выглядеть больше, стопкой варнингов или не подсвечивать конкретное место, оставляя гадать, какое же это поле.. Но это то малое, что уже можно использовать сейчас, и расширить и углубить в перспективе. Не раз и не два мы попадались на ошибки соответствия полей, аргументов и их текстового нейминга, а также банальных опечаток.

Однако, нужно отдавать отчет, что использовать constexpr‑парсинга означает более сложную сборку и потенциально более долгий этап компиляции (особенно при больших схемах и сложных запросах). Имеет смысл поддерживать подмножества SQL, достаточные для основных операций. Тем не менее переотправка большей части логики в компилятор — оправданная плата за отсутствие неожиданных ошибок в рантайме.

Расширения и будущее

Новый стандарт C++26 продолжит расширять возможности такого решения. Директива #embed позволит включать файлы схемы или дамп БД прямо в код без внешних скриптов. Расширение constexpr ‑контейнеров позволят использовать динамическую память и умные указатели в constexpr‑контексте. Это означает, что скоро не придется ограничивать число таблиц/столбцов жёсткими массивами — можно будет применять std::vector, std::string.

Помимо этого, возможно сочетание сгенерированного описания схемы с рефлексией в C++26: можно автоматически генерировать классы/типы по описанию таблиц или загружать новые версии схем, не меняя основной код. Тем не менее, текущий вариант в виде интеграции с системами сборки (например, CMake) не в меньше степени гарантирует, что при изменении базы данных создаётся обновлённый заголовок/ресурс, тогда как компиляция проверяет целостность.

Заключение

«Ошибки, предсказанные компилятором, — это ошибки, которые никогда не случились»

Ключевые слова constexpr и consteval — это не только средства для предвычислений, но и полноценный инструмент для чего угодно на этапе компиляции. Применив их к работе с базой данных, мы переносим всю ответственность за корректность SQL‑запросов в компилятор. Проект собирается лишь если все запросы совпадают со схемой. Это устраняет класс ошибок, связанных с базой данных, ещё до запуска приложения. С приходом C++26 эта модель станет ещё мощнее, но уже сейчас это хорошее подспорье для того, чтобы обезопасить рантайм, не усложняя стилистику использования SQL. Поэтому, можно смело сказать:

«Мы привыкли к тому, что constexpr — это математика. Пора понять, что это язык мышления компилятора»

Ссылка на полный проект с тестовыми примерами

Косинцев Артём

Инженер-программист

Теги:
Хабы:
+16
Комментарии15

Публикации

Ближайшие события