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.

the most common cmake commands that you would use professionally

usually, cmake needs to know source folder and build folder

and below are the most common cmake commands that you would use professionally.

  • To config and generate your build folder from your CMakeLists.txt (CMakeLists.txt lives under your project source folder)
$cmake -S . -B build
or just
$cmake -B build

-S is where your project source is located so if you are already in your project source folder you simply typed -S . or just omitted it entirely like the second version so . (dot) mean current dir
-B is where your build output is located. if build folder does not exist cmake will automatically create one.

  • by default, it sets build mode to Release mode so if you want to build debug so you can debug it you can simple tell cmake during build config as seen below
$cmake -B build -DCMAKE_BUILD_TYPE=Debug
  • Last but not least, when you are ready to compile your project you simple tell cmake below
$cmake --buid build

where build is the build folder generated from earlier step

Note:

  • you can add — -j8 to the compilation whcih mean you want it to use 8 compilation jobs in parallel. 8 also means usually 8 processors.
cmake --build build -- -j8
  • if you want to compile specific targets (your binary, linked library etc) you can simple tell cmake
$cmake --build build --target myLib -- -j8
$cmake --build build --target net -- -j8

where myLib, net are what you target as linked libraries in your CMakeLists.txt
e.g:
add_library(mylib STATIC lib.cpp)
add_library(net SHARED net.cpp)

$cmake --build build --target myApp -- -j8

where myApp is what you target as your binary in your CMakeLists.txt
e.g:
add_executable(myapp main.cpp)

  • To do cmake clean but keep build folder
cmake --build build --target clean
  • To do a full cmake clean: you simply remove the build folder and regenerate it
$ rm -rf build
$ cmake -B build

Overwrite a Keyboard Shortcut in VS Code

1. Open the Keyboard Shortcuts menu

Ctrl+K Ctrl+S

This opens the full keybinding editor.

Note: we can also go to File -> Preferences -> Keyboard shortcuts

2. Search for the command we want to change

Use the search bar at the top and type the command name, for example:

Graphviz: Show Preview

or any other command we want to rebind.

3. Click the pencil icon

On the right side of the command row, click the pencil icon to edit the shortcut.

4. Press the new keybinding

VS Code enters “listening mode.”

Press the shortcut you want to assign, such as: e.g

Ctrl+Alt+G

5. Confirm the overwrite

If the shortcut is already used, VS Code will show a conflict message.

Click Yes to overwrite the existing keybinding.

6. (Optional) Edit the JSON directly

If you prefer manual control:

  1. Press Ctrl+Shift+P
  2. Search: Open Keyboard Shortcuts (JSON)
  3. Add your override:

json

{
"key": "ctrl+alt+g",
"command": "graphviz.showPreview"
}

This always overrides the default.

Visualizing CMake Dependencies with Graphviz in VS Code

CMake Graphviz Dependency Graph:

1. Generate the dependency graph

Run this inside our build directory:

cmake --graphviz=dependency.dot 

This creates:

  • dependency.dot: the actual graph
  • dep.* files: internal fragments (safe to delete anytime)

2. Install Graphviz (the rendering engine)

We need this in WSL:

sudo apt install graphviz

This provides the dot command that VS Code uses to render the graph.

3. Install Graphviz Preview extension (the viewer)

In VS Code:

  1. Press Ctrl+Shift+X
  2. Search: Graphviz Preview
  3. Click Install in WSL: Ubuntu

This registers the preview command inside WSL.

4. Open the .dot file in VS Code

Just open:

dependency.dot

5. Render the graph

Press:

Ctrl+Shift+P

Type:

Graphviz: Show Preview

Select it -> our.dot file renders as a graph.

⭐ That’s the entire working pipeline

  • Generate DOT
  • Install Graphviz
  • Install Graphviz Preview
  • Open DOT
  • Run Graphviz Preview

Mocking Unit Test with Catch2 regarding Pub/Sub client and message broker (nats-server) using protobuf cross-platform data format

I have wrote them and posted to my GitHub repos from the official Alpine docker image

testing_nats_msg_broker_pub_sub_client_with_protobuf_alpine/README.md at main · chanvichekaouk/testing_nats_msg_broker_pub_sub_client_with_protobuf_alpine

🧪 Mock Publisher–Broker–Subscriber Unit Test (Catch2 v3 + Protobuf + Alpine) This project demonstrates a mock unit‑test workflow between:

a Publisher client

a NATS message broker

a Subscriber client

using a cross‑platform Protobuf data format, tested with the Catch2 v3 framework and built on the official Alpine Linux image.

The goal is to validate message flow, multi‑subscriber behavior, and protobuf serialization/deserialization in a lightweight, reproducible environment.

🛠️ Compiling Tests Manually (g++) You can compile the test suite directly from the terminal:

g++ -Iinclude tests/*.cpp -o test_runners \
    -I/usr/local/include -L/usr/local/lib \
    -lCatch2Main -lCatch2

Run all tests under a tag:

./test_runners "[tag]"

Run only a specific section under a tag:

./test_runners "[tag]" -c "section_name"

Example:

Run all sections under the tag:

./test_runners "[multi_sub_receive_msg]"

Run only a specific section:

./test_runners "[multi_sub_receive_msg]" -c "Client connects successfully"

Another example:

./test_runners "[multi_sub_receive_msg]" -c "Multiple subscribers receive the same published message"

Screenshots 

image

📦Using Protobuf Data Format

Example .proto schema:

syntax = "proto3";

message Telemetry {
    int32 id = 1;
    int32 temp = 2;
}

🐧 Alpine vs glibc: Why Linking Protobuf Is Harder on Alpine On most Linux distributions that use glibc, linking Protobuf with Abseil is simple. But Alpine uses musl, and Abseil depends on glibc‑specific internals such as futex.

Because musl does not provide these interfaces, Alpine maintainers must:

  • patch Abseil heavily
  • disable unsupported features
  • split Abseil into ~130 micro‑libraries

Starting with Protobuf 4.x, Abseil became a mandatory dependency, which makes linking more complex on Alpine.

On glibc‑based systems (Ubuntu, Debian, Fedora) You can link with just a few Abseil libraries:

-labsl -labsl_strings -labsl_log

On Alpine (musl) You must explicitly link the micro‑libraries required by your generated Protobuf code:

-lprotobuf -lprotobuf-lite
-labsl_base -labsl_strings
-labsl_raw_logging_internal
-labsl_log_internal_check_op
-labsl_log_internal_message
-labsl_log_internal_nullguard

Example:

g++ -Iinclude -Iproto \
proto/*.cc tests/*.cpp \
-o test_runners \
-lprotobuf -lprotobuf-lite \
-labsl_base -labsl_strings \
-labsl_raw_logging_internal \
-labsl_log_internal_check_op \
-labsl_log_internal_message \
-labsl_log_internal_nullguard \
-lCatch2Main -lCatch2
image

Getting Catch2 v3 to work in alpine docker image

I have tried it today and so far Catch2 v2 works either using package installer or through dropping its header file or compiling its library from its github source code.

But for Catch2 v3, it also works but to compile our binary, we have to either use library Catch2Main to autogenerate main or create our own main code because alpine with Catch2 v3 does not work with auto-generation of main using #define CATCH_CONFIG_MAIN flag

test_main.cpp (alpine with catch2 does not work with this kind of auto-generated main flag)

#define CATCH_CONFIG_MAIN
#include <catch2/catch_all.hpp>

main.cpp (works since we defined our own main)

#include <catch2/catch_session.hpp>

int main(int argc, char* argv[]) {
Catch::Session session;
return session.run(argc, argv);
}

Or also work with Library mode with Catch2main to auto generate main

✅ this works since we use Catch2Main library

g++ test_math.cpp -o test -I/usr/local/include -L/usr/local/lib -lCatch2Main -lCatch2 

❌ this does not work as I have verified that #define CATCH_CONFIG_MAIN flag is not working

g++ test_main.cpp test_math.cpp -o test -I/usr/local/include

✅this works since we wrote our own main

g++ main.cpp test_math.cpp -o test -I/usr/local/include -lCatch2

Resize existing VirtualBox HD

This must be done while the VM is powered off.

Caution: it won’t work if your VM has a snapshot. You either have to restore your snapshot then resize or delete your existing snapshot(s) or clone the disk and resize and attached the resize to the current state. Also be careful with it as there is no shrinking support so in case you accidentally resize it virutally to sth much larger than your host hdd then there won’t be easy to revert it (unless you have a snapshot but restoring snapshot you will loss your current state)

PHASE 1 — VirtualBox layer

Find the disk path

In VirtualBox Manager:

  • Select your VM
  • Settings → Storage
  • Click the disk → look at the Location field

Step 1: copy the location of hd you wish to resize

e.g mine is D:\VM\Linux\KaliLinux2025 Clone\KaliLinux2025 Clone.vdi

Step 2: Then go to directory where you installed your virtualbox to

e.g mine is C:\Program Files\Oracle\VirtualBox

3. At command prompt, execute as seen here where I wish to resize mine to 50GB

.\VBoxManage.exe modifyhd "D:\VM\Linux\KaliLinux2025 Clone\KaliLinux2025 Clone.vdi" --resize 50000

You should see something like this:

0%…10%…20%…30%…40%…50%…60%…70%…80%…90%…100%

This enlarges the virtual disk container, but not the partitions inside it.

PHASE 2 — Partition table layer

After resizing the VDI, the guest OS sees a bigger disk, but the partitions still end at the old size.

Step 4: Resize the partitions

For your Kali VM (MBR layout):

  • sda2 = extended partition
  • sda5 = logical LVM partition

You must expand both.

Inside parted:

sudo parted /dev/sda
(parted) resizepart 2 100%
(parted) resizepart 5 100%
(parted) quit

Now the partition table matches the new disk size.

PHASE 3 — LVM layer

Step 5: Resize the physical volume (PV)

sudo pvresize /dev/sda5

This tells LVM: “The partition is bigger now — use the new space.”

Step 6: Resize the logical volume (LV)

sudo lvextend -l +100%FREE /dev/mapper/kali--vg-root

This expands the LV to fill all free space in the VG.

PHASE 4 — Filesystem layer

Step 7: Resize the filesystem

For ext4:


sudo resize2fs /dev/mapper/kali--vg-root

This expands the filesystem to fill the LV.

FINAL CHECK

df -h /

C++ interface (pure abstract class) && C# interface

1- C# Interface

public interface IShape
{
double Area();
double Perimeter();
}

2- C# Implementation (Rectangle)

public class Rectangle : IShape
{
private readonly double width;
private readonly double height;

public Rectangle(double width, double height)
{
this.width = width;
this.height = height;
}

public double Area() => width * height;

public double Perimeter() => 2 * (width + height);
}

3- Using the C# interface

IShape shape = new Rectangle(3.0, 4.0);
Console.WriteLine($"Area: {shape.Area()}");
Console.WriteLine($"Perimeter: {shape.Perimeter()}");

A- C++ Interface (pure abstract class)

class IShape {
public:
virtual ~IShape() = default;
virtual double area() const = 0;
virtual double perimeter() const = 0;
};

B- C++ Implementation of the Interface

class Rectangle : public IShape {
public:
Rectangle(double width, double height)
: w_(width), h_(height) {}

double area() const override {
return w_ * h_;
}

double perimeter() const override {
return 2 * (w_ + h_);
}

private:
double w_;
double h_;
};

C- Using the Interface in C++

#include <iostream>
#include <memory>

int main() {
std::unique_ptr<IShape> shape = std::make_unique<Rectangle>(3.0, 4.0);

std::cout << "Area: " << shape->area() << "\n";
std::cout << "Perimeter: " << shape->perimeter() << "\n";
}

Configure GitHub in your Linux system using public\private ssh keys instead of username/password

🟩 Step 1 — Generate your SSH key pair (public key .pub and private key) at ~/.ssh/

ssh-keygen -t ed25519 -C "your_email@example.com"

🟩 Step 2 — Start the SSH agent

eval "$(ssh-agent -s)"

You should see something like:

Agent pid 1234

🟩 Step 3 — Copy your pub key to GitHub

cat ~/.ssh/id_ed25519.pub

copy the conent of your .pub key to GitHub

Go to your github profile ➤ Settings ➤ SSH and GPG kyes ➤ New SSH key

Paste the content of your pub key in text box above under the word “Key” and give it a name in textbox above under the word “Title”

🟩 Step 4 — tell SSH client which private key to use which is paired to the pub key used at GitHub earlier

e.g under current’s user .ssh folder

mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/config

Add:

Host *
AddKeysToAgent yes
IdentityFile ~/.ssh/id_ed25519

ctrl-o to write and ctrl-x to close nano.

Then secure it:

chmod 600 ~/.ssh/config

🟩 Step 5 – testing it

ssh -T git@github.com

you should see sth like this:

Hi github_username! You've successfully authenticated, but GitHub does not provide shell access.

Linux Tree command

tree -f -I build

where -f is to include file with full path. -I mean to exclude specific folder

I do like this util tool in Linux though it does not always come with the distro and I have to install it.

e.g: sample output from my kali linux VM:

┌──(root㉿kali)-[/home/catch2-mini-book/project]
└─# tree -f -I build
.
├── ./CMakeLists.txt
├── ./external
│   ├── ./external/catch_amalgamated.cpp
│   └── ./external/catch_amalgamated.hpp
├── ./id_git_hub_chanvicheka_ouk
├── ./id_git_hub_chanvicheka_ouk.pub
├── ./include
│   ├── ./include/account.hpp
│   ├── ./include/logger.hpp
│   ├── ./include/logic.hpp
│   ├── ./include/service.hpp
│   └── ./include/shape.hpp
├── ./README.md
├── ./src
│   └── ./src/logic.cpp
├── ./test_example
├── ./test_main.o
├── ./test_runner
└── ./tests
├── ./tests/test_example.cpp
├── ./tests/test_main.cpp
├── ./tests/test_oop_composition_class_with_dependencies.cpp
├── ./tests/test_oop_inheritance_polymorphism.cpp
├── ./tests/test_oop_simple.cpp
├── ./tests/test_oop_using_fixture.cpp
└── ./tests/test_parameterization.cpp

5 directories, 22 files