C# Delegates vs C++ Lambdas: A Practical Guide for Developers Who Use Both

Modern C++ and C# both let you treat functions as first‑class citizens. you can store them, pass them around, and call them later (callback mostly we will use it for professionally). But the way each language does this is very different.

If you’ve ever wondered how C# delegates, Func<>, Action<>, lambda expressions, function pointers, functors, and C++ lambdas all relate to each other, this post gives you the complete picture.

1. Delegates in C#: The Type‑Safe Function Pointer

In C#, a delegate is essentially a type‑safe function pointer. It can point to:

  • static methods
  • instance methods
  • lambdas
  • anonymous functions

Example:

public delegate int MyDelegate(int x);

int AddOne(int x) => x + 1;

MyDelegate d = AddOne;
Console.WriteLine(d(5)); // 6

A delegate stores both:

  • a pointer to the method
  • a reference to the object (if it’s an instance method)

This makes delegates more powerful than raw C++ function pointers.

2. Func<> and Action<>: Built‑In Delegate Types

C# gives you built‑in delegate types so you don’t need to declare your own.

Func<arg, return>

Func<int, int> f = x => x * 2;
Console.WriteLine(f(10)); // 20

Action<arg>

Action<int> print = x => Console.WriteLine(x);
print(5);

These are the most common delegate types in modern C#.

3. C++ Function Pointers: The Old‑School Way

C++ also has function pointers, but they are limited:

int AddOne(int x) { return x + 1; }

int (*fp)(int) = &AddOne;
std::cout << fp(5); // 6

Function pointers cannot:

  • store object instances
  • capture variables
  • behave like closures

They only point to free functions or static methods, not instance method.

4. C++ Functors: The Delegate Equivalent

Before lambdas existed, C++ used functors which is a structs with operator().

struct MyDelegate {
    int operator()(int x) const {
        return x + 1;
    }
};

MyDelegate d;
std::cout << d(5); // 6

This is the closest equivalent to a C# delegate type.

5. C++ Lambdas: The Modern Way

C++ lambdas are syntactic sugar for functors.

auto f = [](int x) {
    return x * 2;
};

std::cout << f(10); // 20

Internally, the compiler generates something like:

struct __Lambda {
    int operator()(int x) const { return x * 2; }
};

__Lambda f;

So a C++ lambda is literally a functor object with an inline operator().

6. Closures: Capturing Variables (C# vs C++)

C# automatically captures variables:

int y = 10;
Func<int, int> f = x => x + y;

C++ requires explicit capture:

int y = 10;
auto f = [y](int x) {
    return x + y;
};

C++ gives you full control:

  • [y] capture by copy
  • [&y] capture by reference
  • [=] capture everything by copy
  • [&] capture everything by reference
  • [this] capture the object pointer

This explicitness is crucial in async networking.

7. Capturing this: Calling Member Functions

C#

Action a = () => this.DoSomething();

C++

auto a = [this]() {
    DoSomething();
};

C++ requires [this] because lambdas do not automatically have access to member functions.

8. Async Callbacks: C# vs C++ Side‑by‑Side

C# async callback

csharp

stream.ReadAsync(buffer).ContinueWith(task => {
    Process(buffer);
});

C++ async callback (Boost.Asio)

cpp

auto buf = std::make_shared<std::string>();

asio::async_read(stream, asio::buffer(buf->data(), len),
    [this, buf](auto ec, auto bytes) {
        Process(*buf);
    }
);

Both:

  • run later
  • capture variables
  • keep buffers alive
  • call methods

This is where lambdas shine in both languages.

9. Avoiding Lambdas in C++ (If You Want)

If lambdas feel complicated, you can use std::bind:

cpp

asio::async_read(stream, asio::buffer(buf->data(), len),
    std::bind(&MyClass::OnReadComplete, this, buf,
              std::placeholders::_1,
              std::placeholders::_2)
);

Or a functor:

struct Handler {
    MyClass* self;
    std::shared_ptr<std::string> buf;

    void operator()(auto ec, auto bytes) {
        self->Process(*buf);
    }
};

But lambdas are cleaner once you understand them.

10. The Complete Mapping Table

ConceptC#C++
Inline functionx => x * 2[](int x){ return x * 2; }
Function with returnFunc<int,int>auto f = [](int){}
Function with no returnAction<int>auto f = [](int){}
Delegate typedelegate int D(int)struct { int operator()(int); }
Closurecaptures variables[x](){}
Capturing this() => this.Method()[this](){ Method(); }
Async callbackContinueWith(...)async_read(..., [](){})

Note: c++ limbda return type is option e.g

[](int x) -> int { return x * 2; }

is the same as

[](int x){ return x * 2; }

since return type is optional.

Final Thoughts

C# and C++ both give you powerful tools for treating functions as objects, but they approach the problem differently:

  • C# delegates are object‑aware function pointers.
  • Func<> and Action<> are built‑in delegate types.
  • C++ lambdas are functors with inline operator().
  • C++ captures give you precise control over lifetime and memory.
  • Async callbacks in both languages rely heavily on lambdas/closures.

Once you understand the mapping, switching between C# and C++ becomes natural, especially when writing asynchronous, event‑driven, or callback‑heavy code.

Leave a Reply

Your email address will not be published. Required fields are marked *