Skip to content

Translate lambda as struct with operator_call - #376

Open
lucic71 wants to merge 48 commits into
Cpp2Rust:masterfrom
lucic71:lambdas
Open

lucic71 wants to merge 48 commits into
Cpp2Rust:masterfrom
lucic71:lambdas

Conversation

@lucic71

@lucic71 lucic71 commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

This PR translates a lambda []() { return 42; } as:

#[derive(Clone, Default)]
pub struct lambda_0 {}
impl lambda_0 {
    pub fn operator_call() -> i32 {
        return 42;
    }
}

Each captured variable becomes a field of the struct: by value captures are plain values and by reference captures are pointers. Captured this becomes this_ inside the translated body. Captureless lambdas additionally have an associated method called to_free_function that is used to convert between lambda and free function.

Every lambda implementes the libcc2rs::Callable trait so that rules can call into user-defined lambdas. Ideally we would implement the Fn trait for lambda_0, but that is not available in stable Rust.

This fixes #314. In the new translation, the body of lambda functions is translated only once and does not create borrow conflicts.


Notes about the implementation:

Inside the body of the lambda, clang represents usages of captured variables as a DeclRefExpr that points to a VarDecl outside the lambda. Clang also creates FieldDecl's inside the lambda that decide the capture type: by value/by reference. All this information is used in VisitDeclRefExpr.

@lucic71
lucic71 marked this pull request as draft September 17, 2026 11:48
@lucic71
lucic71 marked this pull request as ready for review September 17, 2026 12:29
@nunoplopes

Copy link
Copy Markdown
Contributor

Instead of going this way, can't you capture a pointer by value instead to keep using Rust closures?

@lucic71

lucic71 commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

Instead of going this way, can't you capture a pointer by value instead to keep using Rust closures?

Capturing the pointer by value only fixes the borrowing problem. But it does not fix the following patterns that I need to support in following PRs:

  • generic lambdas [](auto x) { ... } and template lambdas []<typename T>(T t) { ... }
  • decltype(lambda)
  • lambda stored as a struct field
  • templates that receive multiple callable forms: std::less/user-defined functor/lambda at the same time

@nunoplopes

Copy link
Copy Markdown
Contributor

Instead of going this way, can't you capture a pointer by value instead to keep using Rust closures?

Capturing the pointer by value only fixes the borrowing problem. But it does not fix the following patterns that I need to support in following PRs:

  • generic lambdas [](auto x) { ... } and template lambdas []<typename T>(T t) { ... }
  • decltype(lambda)
  • lambda stored as a struct field
  • templates that receive multiple callable forms: std::less/user-defined functor/lambda at the same time

Can you give concrete examples in C++ and in Rust to show why they break?

@lucic71

lucic71 commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

Instead of going this way, can't you capture a pointer by value instead to keep using Rust closures?

Capturing the pointer by value only fixes the borrowing problem. But it does not fix the following patterns that I need to support in following PRs:

  • generic lambdas [](auto x) { ... } and template lambdas []<typename T>(T t) { ... }
  • decltype(lambda)
  • lambda stored as a struct field
  • templates that receive multiple callable forms: std::less/user-defined functor/lambda at the same time

Can you give concrete examples in C++ and in Rust to show why they break?

  1. generic lambdas [](auto x) { ... } and template lambdas []<typename T>(T t) { ... }
auto f = [](auto x) { return x; };
f(1);
f(2.5);

Closures in rust cannot be generic. The argument must be a fixed type.

let f = |x| x;
f(1);
f(2.5);

This translation fails on f(2.5) because the argument is already bound to i32 from the first call.

The auto lambda is a template lambda in disguise []<typename T>(T x) { return x; }. So using the operator_call translation, we translate each instantiation of the lambda and call it accordingly:

let f_i32 = lambda_i32 {};
let f_f32 = lambda_f32{};
f_i32(1);
f_f32(2.5);
  1. decltype(lambda)
auto cmp = [](int a, int b) { return a > b; };
std::set<int, decltype(cmp)> s;

Captureless lambdas are default constructible. This is relevant for the constructor of set for example that does:

set()
    : set(Compare()) {}

Rust closures are not defaultable.

  1. lambda stored as a struct field
template <typename Pred>
struct S {
  Pred p; // p has type of lambda
  int call() { return p(1, 2); }
};

S<decltype([](int a, int b) { return a + b; })> s;
s.call();

Each field must have a concrete type. Closures don't have a spellable type:

struct S_??? {
  pub p: ???;
}

With lambdas translated as structs the translated code becomes:

#[derive(Default, Copy, Clone)]
struct S_lambda_1 {
  pub p: lambda_1;
}
  1. templates that receive multiple callable forms: std::less/user-defined functor/lambda at the same time
template <typename Cmp> int f(int a, int b, Cmp cmp) { return cmp(a, b) ? a : b; }

f(1, 2, std::less<int>{});
f(1, 2, MyCustomComparator{});
f(1, 2, [](int a, int b) { return a > b; });

MyCustomComparator is a struct + operator_call. Having the lambda translated in the same way makes the translation uniform.

To summarize, lambdas are a superset of closures so they don't map directly in the generated code.

@nunoplopes

Copy link
Copy Markdown
Contributor

I don't see a hard reason in any of those examples. For templates, you need multiple closures, which is not different than creating multiple functions.
For the type of closures, they can have a type. If nothing else, they can become a function pointer and stored in a struct field.

Translating lambdas into functions is very annoying. Plus you lose the closure part; you need to add additional arguments to the function.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Lambdas in unsafe are inlined at every call site

2 participants