List Comprehensions in C++26 with a Declarative Programming Style

List Comprehensions in C++26 with a Declarative Programming Style

Usage:

// ============================================================
// USER CODE
// ============================================================
int main()
{
    using namespace ListComprehension;

    // ============================================================
    // std::vector
    // ============================================================
    std::vector<int> nums{ 1, 2, 3, 4 };
    auto squares = in(nums).each(x * x);
    Print(squares);
    
    auto filtered = in(nums).only(x < 3).each(x * x);
    Print(filtered);

    // ============================================================
    // std::array
    // ============================================================
    std::array<int, 4> array{ 5, 6, 7, 8 };
    auto arraySquares = in(array).each(x * x);
    Print(arraySquares);

    // ============================================================
    // std::list
    // ============================================================
    std::list<int> list{ 10, 20, 30 };
    auto listSquares = in(list).each(x * x);
    Print(listSquares);

    // ============================================================
    // std::set
    // ============================================================
    std::set<int> set{ 3, 4, 5 };
    auto setSquares = in(set).only(x < 5).each(x * x);
    Print(setSquares);

    // ============================================================
    // std::map
    // ============================================================
    std::map<int, int> map { { 1, 10 }, { 2, 20 }, { 3, 30 } };

    auto keys = in(map).each(first);
    Print(keys);
    
    auto values = in(map).each(second);
    Print(values);

    auto products = in(map).each(first * second);
    Print(products);

    auto filteredProducts = in(map).only(first < 3).each(first * second);
    Print(filteredProducts);

    // ============================================================
    // std::unordered_map
    // ============================================================
    std::unordered_map<int, int> unorderedMap { { 4, 40 }, { 5, 50 }, { 6, 60 } };

    auto unorderedProducts = in(unorderedMap).each(first * second);
    Print(unorderedProducts);
    
    auto filteredUnorderedProducts = in(unorderedMap).only(first < 6).each(first * second);
    Print(filteredUnorderedProducts);

    // ============================================================
    // std::deque
    // ============================================================
    std::deque<int> deque{ 7, 8, 9, 10 };
    auto dequeSquares = in(deque).each(x * x);
    Print(dequeSquares);

    // ============================================================
    // std::string
    // ============================================================
    std::string text = "Hello";
    auto characters = in(text).each(x);
    Print(characters);

    return 0;
}
The Library:

namespace ListComprehension
{
    // ============================================================
    // CORE EXPRESSIONS
    // ============================================================

    struct Expression
    {
    };

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

        template<typename Value>
        constexpr auto operator()(Value) const
        {
            return value;
        }
    };

    template<typename Function>
    struct FunctionExpression : Expression
    {
        Function function;

        template<typename Value>
        constexpr auto operator()(Value value) const
        {
            return function(value);
        }
    };

    template<typename Function>
    constexpr auto makeExpression(Function function)
    {
        return FunctionExpression<Function>
        {
            {},
            std::move(function)
        };
    }

    struct Placeholder : Expression
    {
        template<typename Value>
        constexpr auto operator()(Value value) const
        {
            return value;
        }
    };

    inline constexpr Placeholder x{};

    struct AlwaysTrue : Expression
    {
        template<typename Value>
        constexpr bool operator()(Value) const
        {
            return true;
        }
    };

    template<typename T>
    struct IsExpression : std::is_base_of<Expression, std::remove_cvref_t<T>>
    {
    };

    template<typename T>
    inline constexpr bool IsExpressionV = IsExpression<T>::value;

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

        if constexpr (IsExpressionV<Type>)
        {
            return std::forward<T>(value);
        }
        else
        {
            return ValueExpression<Type>
            {
                {},
                std::forward<T>(value)
            };
        }
    }
    
    // ============================================================
    // MEMBER ACCESS EXPRESSIONS
    // ============================================================

    struct FirstExpression : Expression
    {
        template<typename Pair>
        constexpr decltype(auto) operator()(Pair&& pair) const
        {
            return std::forward<Pair>(pair).first;
        }
    };

    struct SecondExpression : Expression
    {
        template<typename Pair>
        constexpr decltype(auto) operator()(Pair&& pair) const
        {
            return std::forward<Pair>(pair).second;
        }
    };

    inline constexpr FirstExpression first{};
    inline constexpr SecondExpression second{};

    // ============================================================
    // OPERATORS
    // ============================================================

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

        template<typename Value>
        constexpr auto operator()(Value value) const
        {
            return left(value) * right(value);
        }
    };

    template<typename Left, typename Right>
    constexpr auto operator*(Left&& left, Right&& right)
    {
        using LeftExpression =
            decltype(ToExpression(std::forward<Left>(left)));

        using RightExpression =
            decltype(ToExpression(std::forward<Right>(right)));

        return MultiplyExpression<LeftExpression, RightExpression>
        {
            {},
            ToExpression(std::forward<Left>(left)),
            ToExpression(std::forward<Right>(right))
        };
    }

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

        template<typename Value>
        constexpr auto operator()(Value value) const
        {
            return left(value) < right(value);
        }
    };

    template<typename Left, typename Right>
    constexpr auto operator<(Left&& left, Right&& right)
    {
        using LeftExpression =
            decltype(ToExpression(std::forward<Left>(left)));

        using RightExpression =
            decltype(ToExpression(std::forward<Right>(right)));

        return LessExpression<LeftExpression, RightExpression>
        {
            {},
            ToExpression(std::forward<Left>(left)),
            ToExpression(std::forward<Right>(right))
        };
    }

    // ============================================================
    // QUERY
    // ============================================================

    template<typename Container, typename Condition = AlwaysTrue>
    struct Query
    {
        const Container& container;
        Condition condition;

        template<typename NewCondition>
        constexpr auto only(NewCondition newCondition) const
        {
            using ExpressionType =
                decltype(ToExpression(newCondition));

            return Query<Container, ExpressionType>
            {
                container,
                ToExpression(newCondition)
            };
        }

        template<typename ExpressionType>
        auto each(ExpressionType expression) const
        {
            auto expr = ToExpression(expression);

            using ResultType =
                decltype(expr(*container.begin()));

            std::vector<ResultType> result;

            for (auto&& value : container)
            {
                if (condition(value))
                {
                    result.push_back(expr(value));
                }
            }

            return result;
        }
    };

    template<typename Container>
    constexpr auto in(const Container& container)
    {
        return Query<Container>
        {
            container,
            {}
        };
    }

    // ============================================================
    // UTILITIES
    // ============================================================

    template<typename Container>
    void Print(const Container& container)
    {
        for (auto&& value : container)
        {
            std::cout << value << " ";
        }

        std::cout << "\n";
    }  
}

Tutorial

This tutorial presents the design and implementation of a small functional programming framework for C++26. The goal is to bring the expressive power of list comprehensions from languages such as Python, Haskell, and functional query languages into modern C++ while preserving static typing and zero-overhead abstractions.

The framework allows operations on containers to be written declaratively:


auto squares =
    in(nums)
        .each(x * x);

auto filtered =
    in(nums)
        .only(x < 3)
        .each(x * x);

Instead of manually writing:


std::vector<int> result;

for (auto value : nums)
{
    if (value < 3)
    {
        result.push_back(value * value);
    }
}

the programmer describes what transformation should happen, while the framework handles iteration and execution.

Goals of the Framework

The design focuses on several goals:

  • Use normal C++ syntax instead of macros or a separate language.
  • Build expressions at compile time using templates.
  • Avoid runtime polymorphism and virtual calls.
  • Support all standard iterable containers.
  • Allow expressions to be composed naturally using operators.
  • Provide a path toward automatic member access using C++26 reflection.

The Basic Idea

The core idea is that expressions are not evaluated immediately. Instead, the framework builds a small object representing the computation.

For example:


x * x

does not multiply anything at the point where it is written. There is no value yet. Instead, the compiler creates an expression tree similar to:


        MultiplyExpression

             *
            / \
           x   x

When the container is processed later, the expression receives the current element and performs the actual computation.

Expression Templates

This technique is known as expression templates. Instead of storing the result of an operation, C++ stores the structure of the operation itself.

A simple example outside this library would be:


auto expression = a + b * c;

A traditional implementation evaluates this immediately:


temporary1 = b * c
result = a + temporary1

An expression template implementation instead creates:


AddExpression
|
+-- a
|
+-- MultiplyExpression
    |
    +-- b
    |
    +-- c

The computation is delayed until a value is actually needed.

The List Comprehension Syntax

The framework exposes two primary operations:

each()

The each() operation transforms every element in the input collection.


std::vector<int> nums{1,2,3,4};

auto squares =
    in(nums)
        .each(x * x);

Conceptually this represents:


[value * value for value in nums]

only()

The only() operation filters elements before the transformation is performed.


auto filtered =
    in(nums)
        .only(x < 3)
        .each(x * x);

Conceptually:


[value * value for value in nums if value < 3]

The Fluent Query Model

The original implementation used:


in(container, each(expression));

The framework was redesigned into a fluent query model:


in(container)
    .only(condition)
    .each(expression);

This gives the library a natural place to add future operations:


in(container)
    .only(condition)
    .each(expression)
    // future:
    // .sort()
    // .take()
    // .skip()
    // .sum()
    // .groupBy()

The query object becomes a description of the computation pipeline, while execution happens when a terminal operation such as each() is called.


Expression Objects

Every expression in the framework is represented by a small object. These objects are combined together to form larger expressions.

The common base type is only a marker:


struct Expression
{
};

This allows the framework to distinguish between values and expressions.

For example:


x * 5

contains two different kinds of objects:

  • x is already an expression.
  • 5 is a normal value and must be wrapped into an expression.

Converting Values Into Expressions

The framework needs a way to automatically convert normal C++ values into expression objects. This allows operators to work with both expressions and constants.

For example, this expression:


x * 5

is internally transformed into:


MultiplyExpression

        *
       / \
      x   ValueExpression<int>
              |
              5

The conversion is handled by ToExpression().


template<typename T>
struct IsExpression :
    std::is_base_of<Expression, std::remove_cvref_t<T>>
{
};

template<typename T>
inline constexpr bool IsExpressionV =
    IsExpression<T>::value;


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

    if constexpr (IsExpressionV<Type>)
    {
        return std::forward<T>(value);
    }
    else
    {
        return ValueExpression<Type>
        {
            {},
            std::forward<T>(value)
        };
    }
}

If the object is already an expression, it is returned unchanged. Otherwise it is wrapped inside a ValueExpression.

Constant Expressions

The simplest expression type stores a value and always returns it.


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

    template<typename Value>
    constexpr auto operator()(Value) const
    {
        return value;
    }
};

Notice that the input value is ignored:


ValueExpression<int> expression{ {}, 10 };

expression(5);
expression(100);
expression(999);

All three calls return:


10

This is what allows constants to participate in larger expression trees.

The Placeholder Expression

The special object x represents the current element being processed.


struct Placeholder : Expression
{
    template<typename Value>
    constexpr auto operator()(Value value) const
    {
        return value;
    }
};

inline constexpr Placeholder x{};

When the framework evaluates:


x * x

both occurrences of x receive the current container element.

For example, with:


std::vector<int> nums
{
    1,2,3,4
};

the evaluation becomes:


1 * 1
2 * 2
3 * 3
4 * 4

Function Expressions

The framework can also wrap normal C++ functions or lambdas into expressions.


template<typename Function>
struct FunctionExpression : Expression
{
    Function function;

    template<typename Value>
    constexpr auto operator()(Value value) const
    {
        return function(value);
    }
};

The helper function:


template<typename Function>
constexpr auto makeExpression(Function function)
{
    return FunctionExpression<Function>
    {
        {},
        std::move(function)
    };
}

allows arbitrary computations to become part of the expression system.

For example:


auto doubled =
    makeExpression(
        [](int value)
        {
            return value * 2;
        });

Now the lambda behaves exactly like the built-in expression objects.

Operator Expressions

The next step is making normal C++ operators construct expression trees.

The multiplication operator creates a MultiplyExpression.


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

    template<typename Value>
    constexpr auto operator()(Value value) const
    {
        return left(value) * right(value);
    }
};

The expression stores two child expressions:


MultiplyExpression

        *
       / \
   left   right

When evaluated, it evaluates both children and multiplies the results.

The operator overload builds the expression:


template<typename Left, typename Right>
constexpr auto operator*(Left&& left, Right&& right)
{
    using LeftExpression =
        decltype(ToExpression(std::forward<Left>(left)));

    using RightExpression =
        decltype(ToExpression(std::forward<Right>(right)));

    return MultiplyExpression<
        LeftExpression,
        RightExpression
    >
    {
        {},
        ToExpression(std::forward<Left>(left)),
        ToExpression(std::forward<Right>(right))
    };
}

This means:


x * x

is not multiplication yet. It is a compile-time object describing multiplication.


Comparison Expressions

The same technique used for multiplication can be applied to comparisons. The framework can overload operators so that expressions can be used as conditions.

For example:


x < 3

creates a comparison expression instead of immediately performing a comparison.


LessExpression

          <

        /   \
       x     3

The expression is evaluated later when a container element is available.


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

    template<typename Value>
    constexpr auto operator()(Value value) const
    {
        return left(value) < right(value);
    }
};

The operator overload follows the same pattern as multiplication:


template<typename Left, typename Right>
constexpr auto operator<(Left&& left, Right&& right)
{
    using LeftExpression =
        decltype(ToExpression(std::forward<Left>(left)));

    using RightExpression =
        decltype(ToExpression(std::forward<Right>(right)));

    return LessExpression<
        LeftExpression,
        RightExpression
    >
    {
        {},
        ToExpression(std::forward<Left>(left)),
        ToExpression(std::forward<Right>(right))
    };
}

Now the following becomes possible:


in(nums)
    .only(x < 3)
    .each(x * x);

The condition and transformation are both expression trees.


The Query Object

The query object represents a computation over a container.

It stores:

  • The input container.
  • The current filtering condition.

The default condition accepts every element.


struct AlwaysTrue : Expression
{
    template<typename Value>
    constexpr bool operator()(Value) const
    {
        return true;
    }
};

The query type is generic over both the container and the condition.


template<typename Container, typename Condition = AlwaysTrue>
struct Query
{
    const Container& container;
    Condition condition;


};

The only() Filter Operation

The only() function replaces explicit filtering loops.

Example:


auto result =
    in(nums)
        .only(x < 3)
        .each(x * x);

The filter does not execute immediately. Instead, it creates a new query with a different condition.


template<typename NewCondition>
constexpr auto only(NewCondition newCondition) const
{
    using ExpressionType =
        decltype(ToExpression(newCondition));

    return Query<Container, ExpressionType>
    {
        container,
        ToExpression(newCondition)
    };
}

The original query remains unchanged. A new query pipeline is created.

The each() Operation

The each() operation is the terminal operation that executes the query.

It:

  • creates the result container
  • iterates through the source container
  • evaluates the condition
  • evaluates the transformation expression

template<typename ExpressionType>
auto each(ExpressionType expression) const
{
    auto expr = ToExpression(expression);

    using ResultType =
        decltype(expr(*container.begin()));

    std::vector<ResultType> result;

    for (auto&& value : container)
    {
        if (condition(value))
        {
            result.push_back(expr(value));
        }
    }

    return result;
}

The important point is that the loop is still normal C++.

The compiler sees through the expression objects and can optimize the final code exactly like hand-written code.

Creating a Query

The user-facing entry point is the in() function.


template<typename Container>
constexpr auto in(const Container& container)
{
    return Query<Container>
    {
        container,
        {}
    };
}

This starts a query pipeline:


in(nums)

which can then be extended:


in(nums)
    .only(x < 3)
    .each(x * x);

Using Different Standard Containers

Because the implementation only requires range iteration, the same syntax works with many standard library containers.


std::vector<int> nums
{
    1,2,3,4
};

auto squares =
    in(nums)
        .each(x * x);

std::array<int,4> values
{
    5,6,7,8
};

auto result =
    in(values)
        .only(x < 8)
        .each(x * x);

std::list<int> numbers
{
    10,20,30
};

auto result =
    in(numbers)
        .each(x * x);

The framework is not tied to a specific container type. It works with any type that supports range-based iteration.


Associative Containers and Member Access

Sequence containers are straightforward because each element is a single value. Associative containers such as std::map and std::unordered_map are slightly different because each element is a key/value pair.

For example:


std::map<int, int> values
{
    {1,10},
    {2,20},
    {3,30}
};

The elements are:


std::pair<const int, int>

where:

  • first contains the key.
  • second contains the value.

The expression system can expose member access as another expression node.

Member Expression

A member pointer can be converted into a callable expression.


template<typename Member>
constexpr auto member(Member memberPointer)
{
    return makeExpression(
        [memberPointer](auto&& value) -> decltype(auto)
        {
            return value.*memberPointer;
        });
}

This converts normal C++ member access:


value.first

into a reusable expression object.

Convenience helpers can then be created:


template<typename Pair>
constexpr auto first()
{
    return member(&Pair::first);
}

template<typename Pair>
constexpr auto second()
{
    return member(&Pair::second);
}

Selecting Keys and Values

The key of a map can now be selected:


auto keys =
    in(values)
        .each(first<std::pair<const int,int>>());

The framework evaluates this as:


(1,10) -> 1

(2,20) -> 2

(3,30) -> 3

Likewise, values can be extracted:


auto numbers =
    in(values)
        .each(second<std::pair<const int,int>>());

Result:


10
20
30

Composing Member Expressions

Because member access produces a normal expression object, it can be combined with operators.


auto products =
    in(values)
        .each(
            first<std::pair<const int,int>>() *
            second<std::pair<const int,int>>()
        );

The resulting expression tree is:


              *

            /   \

       first   second

          |       |

        key     value

For the map:


(1,10) -> 10

(2,20) -> 40

(3,30) -> 90

The Complete User Experience

The final syntax reads almost like a mathematical description of the operation:


auto result =
    in(map)
        .only(
            first<std::pair<const int,int>>() < 3
        )
        .each(
            first<std::pair<const int,int>>() *
            second<std::pair<const int,int>>()
        );

The meaning is:

From the map, select entries where the key is less than 3, then return key multiplied by value.

Why Expression Templates Instead of Lambdas?

The same operations could be written using lambdas:


auto result =
    std::vector<int>();

for (auto value : nums)
{
    if (value < 3)
    {
        result.push_back(value * value);
    }
}

Or using standard algorithms:


std::ranges::transform(
    nums,
    output.begin(),
    [](auto value)
    {
        return value * value;
    });

Both approaches are valid. The purpose of this framework is not to replace the standard library, but to provide a more declarative syntax.

The expression template approach provides:

  • Composable operations.
  • Operator syntax matching mathematical expressions.
  • A reusable expression tree representation.
  • A foundation for additional query operations.

Future Extensions

The current framework only implements:

  • each() for transformation.
  • only() for filtering.
  • Basic operators.
  • Member access.

However, the architecture naturally supports additional operations.


in(numbers)
    .only(x > 10)
    .each(x * 2)
    .take(5)
    .sum();

Possible future operations include:

  • where() as an alias for filtering.
  • select() as an alias for transformation.
  • take().
  • skip().
  • orderBy().
  • groupBy().
  • aggregate().

C++26 Reflection and Automatic Member Access

One of the most interesting future directions for this framework is integration with C++26 reflection.

Currently, member access requires manually specifying member pointers:


first<std::pair<const int,int>>()

second<std::pair<const int,int>>()

This works, but it requires knowledge of the exact type and the member names that should be accessed.

Reflection changes this by allowing programs to inspect types and their members at compile time.

Current Approach

Today, a member expression is created from a pointer-to-member:


member(&Person::name)

The compiler knows:

  • The object type.
  • The member type.
  • The location of the member.

The expression framework wraps this information into a callable object.


MemberExpression

        |
        |
   member pointer

        |
        |
     object

        |
        |
     member value

Reflection-Based Access

With reflection, member names can become part of the expression system.

The ideal future syntax could become:


in(people)
    .each(x.name);

or:


in(people)
    .only(x.age > 18)
    .each(x.name);

The compiler could transform this into an expression tree where the member lookup is resolved during compilation.


MemberExpression

          name

            |

          Person

            |

          string

Why the Current Architecture Helps

The important design decision was separating:

  • Expression construction.
  • Expression evaluation.
  • Container traversal.

The query engine does not know or care how a value is calculated.


if (condition(value))
{
    result.push_back(expression(value));
}

Whether expression is:

  • a multiplication expression,
  • a comparison expression,
  • a lambda expression,
  • a reflected member access expression,

the query engine remains unchanged.

Expression Trees as a Universal Intermediate Representation

The expression tree acts as a small intermediate representation inside C++.

For example:


x * x + 10

could eventually become:


              +

            /   \

           *     10

         /   \

        x     x

The same idea can represent:

  • Arithmetic.
  • Comparisons.
  • Member access.
  • Function calls.
  • Database-like queries.
  • Serialization operations.

Performance Characteristics

Although the syntax resembles a higher-level functional language, the generated code remains ordinary C++.

For example:


auto result =
    in(nums)
        .only(x < 10)
        .each(x * x);

is conceptually equivalent to:


std::vector<int> result;

for (auto value : nums)
{
    if (value < 10)
    {
        result.push_back(value * value);
    }
}

There are:

  • No virtual function calls.
  • No heap allocation for expressions.
  • No runtime expression parsing.
  • No dynamic dispatch.

The expression objects exist only to help the compiler generate the final code.


Complete Example

The following example demonstrates the complete style of the library.


#include <vector>
#include <map>
#include <iostream>

using namespace ListComprehension;


int main()
{
    std::vector<int> nums
    {
        1,2,3,4
    };


    auto squares =
        in(nums)
            .each(x * x);

    Print(squares);


    auto filtered =
        in(nums)
            .only(x < 3)
            .each(x * x);

    Print(filtered);


    std::map<int,int> values
    {
        {1,10},
        {2,20},
        {3,30}
    };


    auto products =
        in(values)
            .each(
                first<std::pair<const int,int>>() *
                second<std::pair<const int,int>>()
            );

    Print(products);
}

Conclusion

This small framework demonstrates how far modern C++ templates can be pushed toward expressive, functional programming syntax without sacrificing performance.

The core ideas are simple:

  • Expressions are represented as types.
  • Operators build expression trees.
  • Queries execute those expressions over containers.
  • Everything is resolved statically by the compiler.

The design also provides a natural path toward C++26 reflection, where member access and type inspection can become automatic while preserving the same expression framework.

The result is a small embedded query language built entirely from standard C++ features: a bridge between functional programming concepts and zero-overhead native C++.