LINQ style Compile-Time Query DSL Using Reflection and Expression Templates in C++26

A C++26 Compile-Time Query DSL Using Reflection and Expression Templates

Github ↗

The Program


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

struct Record
{
    std::string name;
    int age;
    float weight;
};


int main()
{
    std::vector<Record> records
    {
        {"John",32,75.5f},
        {"Mary",28,61.0f},
        {"Peter",41,83.2f},
        {"Jane",32,59.0f}
    };


    auto query =
        from(records);


    auto result =
        query.where(
            query.name.contains("a")
            &&
            (
                query.age == 32
                ||
                query.weight > 60.0f
            ));


    for(auto&& record : result)
    {
        std::cout
            << record.name
            << '\n';
    }


    return 0;
}

The Library


#include <meta>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>


// ============================================================
// REFLECTION SUPPORT
// ============================================================

template<typename T>
constexpr auto GetMembers()
{
    constexpr auto context =
        std::meta::access_context::current();


    return std::define_static_array(
        std::meta::nonstatic_data_members_of(
            ^^T,
            context));
}


// ============================================================
// TYPE NORMALIZATION
// ============================================================

template<typename T>
using CleanType =
    std::remove_cvref_t<T>;


template<typename T>
struct Normalize
{
    using Type =
        CleanType<T>;
};


template<size_t Size>
struct Normalize<const char(&)[Size]>
{
    using Type =
        std::string;
};


template<typename T>
using NormalizeT =
    typename Normalize<T>::Type;


// ============================================================
// EXPRESSION BASE
// ============================================================

template<typename Derived>
struct Expression
{
    template<typename Value>
    constexpr auto operator==(Value&& value) const;


    template<typename Value>
    constexpr auto operator>(Value&& value) const;


    template<typename Value>
    constexpr auto operator&&(Value&& value) const;


    template<typename Value>
    constexpr auto operator||(Value&& value) const;


    template<typename Value>
    constexpr auto contains(Value&& value) const;
};



template<typename T>
concept ExpressionType =
    std::derived_from<
        CleanType<T>,
        Expression<CleanType<T>>>;


// ============================================================
// VALUE EXPRESSION
// ============================================================

template<typename T>
struct ValueExpression :
    Expression<ValueExpression<T>>
{
    T value;


    template<typename U>
    constexpr ValueExpression(U&& value)
        :
        value(std::forward<U>(value))
    {
    }


    template<typename Row>
    constexpr decltype(auto) Evaluate(Row&&) const
    {
        return value;
    }
};


// ============================================================
// FIELD EXPRESSION
// ============================================================

template<auto Member>
struct FieldExpression :
    Expression<FieldExpression<Member>>
{
    template<typename Row>
    constexpr decltype(auto) Evaluate(Row&& row) const
    {
        return std::forward<Row>(row).[:Member:];
    }
};


// ============================================================
// EXPRESSION CONVERSION
// ============================================================

template<typename T>
constexpr auto ToExpression(T&& value)
{
    using Type =
        CleanType<T>;


    if constexpr(ExpressionType<Type>)
    {
        return std::forward<T>(value);
    }
    else
    {
        using Stored =
            NormalizeT<T>;


        return ValueExpression<Stored>
        {
            std::forward<T>(value)
        };
    }
}


// ============================================================
// OPERATIONS
// ============================================================

struct Equal
{
    template<typename L, typename R>
    static constexpr auto Apply(
        L&& left,
        R&& right)
    {
        return left == right;
    }
};


struct Greater
{
    template<typename L, typename R>
    static constexpr auto Apply(
        L&& left,
        R&& right)
    {
        return left > right;
    }
};


struct Contains
{
    template<typename L, typename R>
    static constexpr auto Apply(
        L&& left,
        R&& right)
    {
        return left.find(right) !=
               std::string::npos;
    }
};


struct LogicalAnd
{
    template<typename L, typename R>
    static constexpr auto Apply(
        L&& left,
        R&& right)
    {
        return left && right;
    }
};


struct LogicalOr
{
    template<typename L, typename R>
    static constexpr auto Apply(
        L&& left,
        R&& right)
    {
        return left || right;
    }
};


// ============================================================
// BINARY EXPRESSION TREE
// ============================================================

template<typename Left, typename Right, typename Operation>
struct BinaryExpression :
    Expression<
        BinaryExpression<Left, Right, Operation>>
{
    Left left;
    Right right;


    constexpr BinaryExpression(
        Left left,
        Right right)
        :
        left(std::move(left)),
        right(std::move(right))
    {
    }


    template<typename Row>
    constexpr auto Evaluate(Row&& row) const
    {
        return Operation::Apply(
            ::Evaluate(left,row),
            ::Evaluate(right,row));
    }
};


// ============================================================
// QUERY OBJECT
// ============================================================

template<typename Container>
struct Query :
    decltype(
        MakeFields<
            typename Container::value_type>())
{
    const Container& source;


    Query(const Container& source)
        :
        source(source)
    {
    }


    template<typename Condition>
    auto where(Condition condition) const
    {
        return FilteredQuery
        {
            source,
            ToExpression(condition)
        };
    }
};


template<typename Container>
auto from(const Container& source)
{
    return Query<Container>
    {
        source
    };
}

Tutorial

This tutorial presents the design and implementation of a small C++26 compile-time query framework. The goal is to create a SQL-like and LINQ-like programming model where queries can be expressed declaratively while keeping all expressions statically typed.

The framework allows code such as:


auto result =
    from(records)
        .where(
            name.contains("a")
            &&
            (
                age == 32
                ||
                weight > 60.0f
            ));

Instead of manually writing:


std::vector<Record> result;

for(auto& record : records)
{
    if(record.name.find("a") != std::string::npos
        &&
        (record.age == 32 ||
         record.weight > 60.0f))
    {
        result.push_back(record);
    }
}

The query expression is built first and evaluated later when the collection is iterated. This separation between describing a query and executing a query is the foundation of the framework.

Expression Templates

The central mechanism is expression templates. Instead of immediately calculating:


q.age == 32

the library creates a type representing the operation:


BinaryExpression
<
    FieldExpression<age>,
    ValueExpression<int>,
    Equal
>

The expression becomes a compile-time representation of the computation. No intermediate values are produced.

The Expression Base Class

All expression objects inherit from the CRTP base:


template<typename Derived>
struct Expression
{
};

CRTP allows the base class to generate operators while preserving the exact derived type. This means operators such as ==, >, and && can work on every expression type without virtual functions or runtime polymorphism.

Reflection Generated Fields

C++26 reflection removes the need to manually declare query fields. The library inspects the data members of the row type:


struct Record
{
    std::string name;
    int age;
    float weight;
};

Reflection discovers these members and generates a field object for each one:


query.name
query.age
query.weight

Each generated field stores the reflected member information and knows how to extract its value from a row:


return row.[:Member:];

Expression Evaluation

Expressions are evaluated only when the query runs. For example:


query.age == 32

creates a tree:


        Equal
       /     \
    age       32

When a row is evaluated, the tree is traversed:


Evaluate(expression,row);

The field expression extracts the member, the value expression returns the constant, and the operation combines the results.

Combining Conditions

Logical expressions create larger expression trees:


q.age == 32
||
q.weight > 60.0f

becomes:


              OR
             /  \
          Equal  Greater
          /  \     /   \
       age  32 weight 60

The query language is therefore naturally composable. New operations can be added by creating another operation object with an Apply function.

Normalization of Values

Values used inside expressions are converted through ToExpression. Existing expressions are preserved, while ordinary values become ValueExpression objects.


query.name.contains("John")

The string literal is converted into a stored std::string, avoiding lifetime issues with temporary character arrays.

Query Execution Model

The query object does not immediately allocate results. Instead, it provides an iterator which skips rows that do not satisfy the condition.


while(current != end &&
      !Evaluate(condition,*current))
{
    ++current;
}

This makes filtering lazy. Rows are tested only when requested by iteration.

Design Advantages

  • Queries are strongly typed at compile time.
  • No string parsing is required like traditional SQL builders.
  • Reflection automatically generates fields.
  • Expression trees can be optimized by the compiler.
  • Operators remain natural C++ syntax.
  • The framework requires no runtime metadata system.

Extending the DSL

Additional operations can be introduced easily:


struct Less
{
    template<typename L, typename R>
    static constexpr auto Apply(
        L&& left,
        R&& right)
    {
        return left < right;
    }
};

The expression system is independent from the actual operations. This separation allows the same infrastructure to support comparisons, arithmetic, string functions, projections, aggregations, and more advanced query features.