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:
xis already an expression.5is 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:
firstcontains the key.secondcontains 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++.