What is a Feed-Forward Neural Network Layer? (With C++ & Python Examples)

June 13th, 2026 No comments

Demystifying the Feed-Forward Neural Network Layer: A Hands-On Guide in C++ and Python

In the world of machine learning and artificial intelligence, neural networks are often treated as black boxes. But beneath the surface, they are built from simple, elegant mathematical blocks. The most fundamental and widely used of these blocks is the Feed-Forward Layer (also known as a Dense or Fully Connected layer).

Whether you are building a simple classifier or training complex deep learning models for financial market prediction, understanding the feed-forward layer is essential.

In this post, we will break down what a feed-forward layer is, how it works, and how to implement it. To make things practical, we will walk through a complete example (solving the classic XOR problem) using a lightweight, dependency-free C++ neural network library, and then compare it with implementations in Python, PyTorch, and TensorFlow.


What is a Feed-Forward Layer?

At its core, a feed-forward layer is a collection of artificial neurons where every input is connected to every output.

Inputs          Hidden Layer (FF)        Output Layer
 (X1)  --------->  (Neuron 1)  --------->  (Output Y)
        \        /          \        /
         \      /            \      /
          \    /              \    /
           \  /                \  /
 (X2)  -----\/-->  (Neuron 2)  -\/

When data passes through a feed-forward layer, three main operations occur:

  1. Weight Multiplication: Each input signal is multiplied by a “weight” representing the strength of the connection.
  2. Bias Addition: A “bias” value is added to the weighted sum. The bias allows the activation function to shift left or right, which is crucial for learning complex patterns.
  3. Activation Function: The combined sum is passed through a non-linear activation function (like Sigmoid, ReLU, or Tanh). This non-linearity allows the network to learn relationships that are more complex than a straight line.

Mathematically, for a given input vector $\mathbf{x}$, the output $\mathbf{y}$ of a feed-forward layer is represented as:

$$\mathbf{y} = f(\mathbf{W}\mathbf{x} + \mathbf{b})$$

Where:

  • $\mathbf{W}$ is the weight matrix.
  • $\mathbf{b}$ is the bias vector.
  • $f$ is the activation function.

The XOR Problem: Our Testing Ground

To demonstrate feed-forward layers in action, we will use the XOR (Exclusive OR) gate. The XOR gate is a classic problem in machine learning because it is linearly inseparable—you cannot separate the outputs ($0$ and $1$) with a single straight line.

To solve it, we need at least one hidden feed-forward layer to warp the input space so that it becomes separable.

Input 1 Input 2 Expected Output
0 0 0
0 1 1
1 0 1
1 1 0

1. The C++ Implementation (Using myoddweb::nn)

For performance-critical systems, trading bots, or resource-constrained environments, C++ is the language of choice. Below is an example using myoddweb::nn, a lightweight, dependency-free C++ neural network library.

Here, we configure a network with:

  • An Input Layer of 2 neurons.
  • One Hidden Feed-Forward Layer of 2 neurons (using Sigmoid activation).
  • An Output Layer of 1 neuron (using Sigmoid activation).
#include <iostream>
#include <vector>
#include "neuralnetwork/neuralnetwork.h"
#include "neuralnetwork/common/logger.h"

using namespace myoddweb::nn;

int main() 
{
  // 1. Define the network topology: 2 inputs, 2 hidden neurons, 1 output
  std::vector<unsigned> topology = { 2, 2, 1 };

  // 2. Define the hidden layer configurations (using Feed-Forward architecture)
  std::vector<LayerDetails> hidden_layers = {
    LayerDetails(
      Layer::Architecture::FF, 
      2, 
      activation(activation::method::sigmoid, 1.0), 
      0.0,                  // Dropout rate (0.0 = disabled)
      0.0,                  // Weight decay
      OptimiserType::SGD, 
      0.99                  // Momentum
    )
  };

  // 3. Define the output layer configuration
  auto output_layer = OutputLayerDetails(
    topology.back(), 
    activation(activation::method::sigmoid, 1.0), 
    ErrorCalculation::type::mse, 
    { 0.0, 0.0, 1.0, 0.0, false, 1.0 }, // Evaluation config
    0.0,                    // Weight decay
    OptimiserType::SGD, 
    0.99                    // Momentum
  );

  // 4. Build the configuration options
  auto options = NeuralNetworkOptions::create(topology)
    .with_batch_size(1)
    .with_output_layer_details(output_layer)
    .with_hidden_layers(hidden_layers)
    .with_learning_rate(0.1)
    .with_number_of_epoch(5000)
    .with_log_level(Logger::LogLevel::Info)
    .build();

  // 5. Create the neural network instance
  NeuralNetwork nn(options);

  // 6. Define training inputs (XOR inputs) and expected outputs
  std::vector<std::vector<double>> inputs = {
    { 0.0, 0.0 },
    { 0.0, 1.0 },
    { 1.0, 0.0 },
    { 1.0, 1.0 }
  };
  std::vector<std::vector<double>> outputs = {
    { 0.0 },
    { 1.0 },
    { 1.0 },
    { 0.0 }
  };

  // 7. Train the network
  std::cout << "Training the neural network...\n";
  nn.train(inputs, outputs);

  // 8. Run inference to check predictions
  std::cout << "\nInference Results:\n";
  for (const auto& input : inputs) 
  {
    auto result = nn.think(input);
    std::cout << "Input: {" << input[0] << ", " << input[1] 
              << "} -> Predicted: " << result[0] << "\n";
  }

  return 0;
}

What Output to Expect

As the network trains over 5000 epochs, the error (mean squared error) steadily decreases. By the end of the training, the output will look something like this:

Training the neural network...
Inference Results:
Input: {0, 0} -> Predicted: 0.0152
Input: {0, 1} -> Predicted: 0.9814
Input: {1, 0} -> Predicted: 0.9815
Input: {1, 1} -> Predicted: 0.0189

Notice how the outputs are extremely close to the expected XOR values ($0$ and $1$).


2. The Python Implementation (Using myoddweb::nn Bindings)

If you prefer Python but want to leverage the performance of the C++ engine, myoddweb::nn includes Python bindings. Here is the same example implemented in Python:

import neuralnetwork as nn

# 1. Define topology
topology = [2, 2, 1]

# 2. Configure hidden and output layers
hidden_activation = nn.Activation(nn.ActivationMethod.Sigmoid, 1.0)
hidden_layers = [
    nn.LayerDetails(
        nn.LayerArchitecture.FF, 
        2, 
        hidden_activation, 
        0.0, 0.0, 
        nn.OptimiserType.SGD, 
        0.99
    )
]

out_activation = nn.Activation(nn.ActivationMethod.Sigmoid, 1.0)
out_layer = nn.OutputLayerDetails(
    topology[-1], 
    out_activation, 
    nn.ErrorCalculationType.MSE,
    nn.EvaluationConfig(),
    0.0, 
    nn.OptimiserType.SGD, 
    0.99
)

# 3. Build neural network options
options = nn.NeuralNetworkOptions.create(topology) \
    .with_batch_size(1) \
    .with_hidden_layers(hidden_layers) \
    .with_output_layer_details(out_layer) \
    .with_learning_rate(0.1) \
    .with_number_of_epoch(5000) \
    .with_log_level(nn.LogLevel.Info) \
    .build()

# 4. Instantiate and train the network
net = nn.NeuralNetwork(options)

training_inputs = [
    [0.0, 0.0],
    [0.0, 1.0],
    [1.0, 0.0],
    [1.0, 1.0]
]
training_outputs = [
    [0.0],
    [1.0],
    [1.0],
    [0.0]
]

print("Training model...")
net.train(training_inputs, training_outputs)

# 5. Evaluate predictions
print("\nInference Results:")
for inputs, expected in zip(training_inputs, training_outputs):
    outputs = net.think(inputs)
    print(f"Input: {inputs} | Expected: {expected[0]} | Predicted: {outputs[0]:.4f}")

How Does This Compare to Other Libraries?

To see how the custom library design compares to major deep learning frameworks, let’s look at the same XOR problem built in PyTorch and TensorFlow.

Option A: PyTorch (Python)

PyTorch is favoured in research due to its pythonic and dynamic nature. In PyTorch, feed-forward layers are represented by the nn.Linear class.

import torch
import torch.nn as nn
import torch.optim as optim

# 1. Define model architecture using nn.Linear (Feed-Forward)
class XORModel(nn.Module):
    def __init__(self):
        super(XORModel, self).__init__()
        self.hidden = nn.Linear(2, 2)  # 2 inputs -> 2 hidden neurons
        self.sigmoid = nn.Sigmoid()
        self.output = nn.Linear(2, 1)  # 2 hidden neurons -> 1 output
        
    def forward(self, x):
        x = self.sigmoid(self.hidden(x))
        x = self.sigmoid(self.output(x))
        return x

model = XORModel()
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.99)

# 2. Data
inputs = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]], dtype=torch.float32)
outputs = torch.tensor([[0.0], [1.0], [1.0], [0.0]], dtype=torch.float32)

# 3. Training
for epoch in range(5000):
    optimizer.zero_grad()
    preds = model(inputs)
    loss = criterion(preds, outputs)
    loss.backward()
    optimizer.step()

# 4. Inference
with torch.no_grad():
    predictions = model(inputs)
    print("PyTorch Results:")
    for inp, pred in zip(inputs, predictions):
        print(f"Input: {inp.tolist()} -> Predicted: {pred.item():.4f}")

Option B: TensorFlow / Keras (Python)

TensorFlow and Keras are widely used in enterprise production settings. Here, the feed-forward layer is represented by the Dense class.

import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.optimizers import SGD

# 1. Define model architecture using Dense (Feed-Forward)
model = Sequential([
    Dense(2, input_dim=2, activation='sigmoid'), # Hidden layer
    Dense(1, activation='sigmoid')              # Output layer
])

# 2. Compile model
model.compile(
    optimizer=SGD(learning_rate=0.1, momentum=0.99), 
    loss='mean_squared_error'
)

# 3. Data
inputs = np.array([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]], dtype=np.float32)
outputs = np.array([[0.0], [1.0], [1.0], [0.0]], dtype=np.float32)

# 4. Training
model.fit(inputs, outputs, epochs=5000, batch_size=1, verbose=0)

# 5. Inference
predictions = model.predict(inputs)
print("TensorFlow/Keras Results:")
for inp, pred in zip(inputs, predictions):
    print(f"Input: {list(inp)} -> Predicted: {pred[0]:.4f}")

Conclusion & Next Steps

Feed-forward layers are the starting point of neural networks, mapping inputs to outputs via simple matrix multiplication and non-linear mappings. While high-level libraries like PyTorch and TensorFlow make it easy to assemble these layers, using a lightweight C++ library like myoddweb::nn on GitHub gives you deep control, portability, and zero-dependency integration.

If you are interested in looking under the hood of neural networks, learning how backpropagation is coded from scratch, or exploring recurrent layers like Elman RNNs and GRUs in native C++, check out the repository, star the project, and start experimenting!

Check out the project on GitHub: FFMG/neural-network

Categories: Neural Network Tags:

How to logout of GitHub Copilot in VS2022

January 13th, 2024 1 comment

If, like me, you have more than one Github account, (one for work and one private), then you might want to switch accounts when using Visual Studio

My private account does not have CoPilot, so it has a little fit when I try and open visual studio.

Now if you click on the link … it takes me to Github and, it doesn’t matter what I do, my credentials are not changed from my private account to my work account.

The solution is simply to force Github to forget about you

  • Close Visual Studio
  • Probably Close VS Code as well … not sure
  • Go to %localappdata%\github-copilot, (your local app data folder, something like C:\Users\%USERNAME%\AppData\Local\github-copilot)
  • Delete the files hosts.json and versions.json
  • Make sure you are logged in the correct Github account, (with Edge or Chrome or whatever).
  • Restart Visual Studio, you should get logged in auto-magically into your ‘correct’ github account, otherwise you can just login when prompted.

And that’s all, if you are not sure about deleting files you can just rename the folder “github-copilot” to something else, and if things go wrong just rename it back.

If you are not sure or comfortable with deleting, renaming and so on … don’t do it. … you probably should not be developing either in that case, but that’s another problem I guess.

Updating from Ubuntu 22.04 to Ubuntu 23.04

April 21st, 2023 Comments off

Now that Ubuntu 23.04 has been released this is the steps to update from 22.04

Of course, this assumes that you are on 22.04.
This guide is very “brief”, if you are not sure what a command does, please look it up first and make sure you know what you are doing.

If you brick your release, don’t blame me, do your homework first 🙂

Update everything

Update everything and install the update managed, (you probably have it already)

sudo apt update && sudo apt upgrade -y && sudo apt install update-manager-core -y

You might be asked a couple of questions, just say yes

When this is done you need to update the manager to tell it that you can update to the next release version

Open the editor and change the upgrade policy

sudo nano /etc/update-manager/release-upgrades

Change the “prompt” to “normal”, (press Ctrl-0 to save and Ctrl-X to exit)

Change the source list to now look at the “lunar” instance instead of “jammy”

sudo sed -i ‘s/jammy/lunar/g’ /etc/apt/sources.list

Update everything … again

sudo apt update && sudo apt upgrade -y

Do the actual update

sudo apt dist-upgrade -y

You will be asked a few questions, it really depends on your environment and setup.

Most questions are fairly straight forward, so say yes to everything.

You will also be asked to remove/replace certain things.
Just read the messages carefully, most of them are fairly simple to understand.

Done

You are now done, so just reboot, (takes a bit longer).

sudo reboot

Once the reboot is done, you can check the version number.

Categories: Ubuntu, Uncategorized Tags:

You cannot call derived member function in the base destructor!

July 25th, 2020 Comments off

Consider the following code …

#include <iostream>

using namespace std;

class Base
{
    public:
    Base(){ cout << "Base Constructor\n";}
    virtual ~Base(){ 
        // the derived class is long gone ...
        This();
        cout << "Base Destructor\n";}
    
    virtual void This(){
        cout << "Base This\n";
    }
};

class Derived : Base
{
    public:
    Derived(){ cout << "Derived Constructor\n";}
    virtual ~Derived(){ cout << "Derived Destructor\n";}
    
    void This() override{
        cout << "Derived This\n";
    }
};

int main()
{
    auto base = new Base();
    delete base;
    
    cout << "-----------------------------\n";
    auto derived = new Derived();
    delete derived;

    return 0;
}

In the code you would be forgiven for thinking that the function “This()” would be called from the derived class, but in fact it is called from the base class

Base Constructor
Base This
Base Destructor
-----------------------------
Base Contructor
Derived Constructor
Derived Destructor
    <--- Derived is now gone ...
Base This
             <--- we are not calling the derived one
Base Destructor

This is because the derived class has been deleted by the time we call the base destructor.

Moral of the story, clean up what you need to cleanup in your derived class, ’cause the base class is not going to do it for you!

Categories: development Tags: ,

Strong Name Signing Your Assemblies

June 21st, 2020 Comments off

What is it for?

Strong naming basically helps to make sure that other assemblies can be 100% sure that they are using the correct assembly and they there has been no messing around

In reality, this not a 100% secure process, but it does help in most cases to add a certain amount of safeguard.

And in any case, a lot of applications require it, and if your app is not strong named, then they will not be able to use it.

How to add strong name to my assemblies

The process is fairly straight forward, (surprisingly so), and you don’t need to worry about Visual Studio until the end.

  • On your system locate the file called sn.exe, (in my case, I had a few of them, the actual version does not matter, but I chose the latest one I have).
  • Open a command line with Administrator privilege, (might not be required, but I don’t know how secure your system is).
  • Navigate to the folder where that exe is located.
What my command line looks like …
  • Create a key … any key, just make sure you give it a name that matters to your application
An example of what it might look like … but really you should use a better name
  • Now get the public key for your application
    sn -p "myassembly.snk" "pmyassembly.snk"
    I added the letter “P” in front of my strong name key … so I know it is the public key … but really, you can call it whatever you want.
Again … use a better name!
  • You might also want your public key as a string value
    sn -tp "pmyassembly.snk"
  • Or you can be one of the cool kids and output to your clipboard
    sn -tp "pmyassembly.snk" |clip
    And then you can paste it where ever
  • I really suggest that you move the file, (with a better name), to your directory
  • Then fire up your visual studio project
    • Look for the properties
    • Select the signing tool
    • And check “sign the Assembly”
The properties if your assemblies
  • Annoyingly enough, Visual Studio will move the file where it feels it should be, (next to the *.csproj), if you really want it somewhere else, just edit the *.csproj file and change the location yourself. Or don’t, whatever.
  • Build it
  • Ship it

What about my Unit tests?

If you have unit tests running and you are testing internal classes … then you will need to add the public key, (see above), to your InternalsVisibleTo( … )

At the end of the day, this is pretty much what consumers of your assembly will do.

What info must I share?

  • Your *.snk file
  • Your public key text value

Remember, this is just to verify the identity of your file, not to do some NSA proof securing of some sort.
You want to share the information so others can use it in their own projects.

More

Categories: development Tags: , ,

How to add command line arguments to your piger commands

April 1st, 2020 Comments off

First the basics

The first argument is the command we wish to execute, the second one is the command line argument and the last one is if we wish to run as administrator or not, (with privileged access).

How to get the arguments?

You have various commands to get the arguments entered.

You can get the ones typed by the user

Or you can also get the selected folder if there is one

Remember that we could have selected more than one folder

Putting it all together

If you have an app like cmder or even the default command line app you can put it all together

And save the file and call it “cmder.lua” and save it in your root command folder, (or subdirectory).

Then if the user types

  • cmder home – they will go to their home directory
  • cmder – they will go to their system drive
  • cmder (with the cursor over a folder name) – they will go to that folder.

More?

You can get more information on the piger github page

Categories: Cheat-sheet, Myoddweb Piger Tags: ,

Design principles – SOLID

February 29th, 2020 Comments off

We often ask people in interview to explain the solid principles and what they mean.

The other day I found a series that goes into details about the SOLID principles

Those are rather long videos, but they go into great details explaining the SOLID principle.

C++ Equivalent of SpinWait.SpinUntil

November 30th, 2019 Comments off

I like to use threads in my code, but the problem with threads is that sometimes you need to wait for them to finish.

In c++ it is very easy to do.

std::thread t (my_function,1);
// do something
// do something else
t.join();

All the code above does is, start a thread, call the function my_function and passes the argument 1.
The t.join(); tells the main thread to wait until the thread is finished.

Now the problem is that t.join() could hang forever … and I would never know about it.

What I needed was something like join_until( 1000) where we either wait for the thread to finish or wait for 1000ms. What I needed was something similar to c# SpinUntil

In very simple terms all SpinUntil does is check a condition, (very often), and if it is true then return (true), but if a timeout happens then we return (false).
So in my case I would wait for the thread to complete, (yes I know, I know, you can’t easily tell if a std::thread is complete or not.

Enter Wait.SpinUntil …

static bool SpinUntil(std::function<bool()> condition, const long long milliseconds)
{
  // magic
}

Basically we will

  1. Start a new thread
  2. Check if the given condition is true
    1. If the condition is true, return true
    2. If the condition is false … continue.
  3. Wait for a couple of nanoseconds for other threads to do their bit
  4. Look how long we have been spinning
    1. If less than milliseconds go back to number 2
    2. If more than milliseconds, return false.

The code

This is a very simplified version of the code, you can find a working solution on my github page

// this code is inside our running thread
// while we will join our thread, it is possible for it to block, (see below)

{
    // assume that we will timeout
    bool result = false;
    std::unique_lock<std::mutex> lock(_mutex);
    try
    {
      // one ms wait.
      const auto oneMillisecond = std::chrono::milliseconds(1);

      // when we want to sleep until.
      const auto until = std::chrono::high_resolution_clock::now() + std::chrono::milliseconds(milliseconds);
      for (auto count = 0; count < std::numeric_limits<int>::max(); ++count)
        {
        if (condition())
        {
          // we have not timeout!
          result = true;
          break;
        }

        if (count % 4 == 0)
        {
          // slee a little bit
          std::this_thread::sleep_for(oneMillisecond);
        }
        else
        {
          // yield.
          std::this_thread::yield();
        }

        // are we done?
        if (std::chrono::high_resolution_clock::now() >= until)
        {
          break;
        }
      }
    }
  }
  catch(...)
  {
    //  something broke ... maybe we should re-throw.
    result = false;
  }
  // return the result
  return result;
}

So now the code above will wait for a couple of ms while running in the background and if the given condition is true we will get out and return true, otherwise we will return false after a while.

Possible issues

  • For one thing, the condition() function could hang, it is up to the caller to make sure this never happens.
  • The condition() function could take a rather long time to execute … it is up to the caller to make sure this never happens

In other words, don’t stuff things up in your condition() function.

Categories: development Tags: , , ,

Add Google test directly to your Visual Studio project

November 9th, 2019 Comments off

Why?

While it is easy(ish) enough to add google test using various packages, you might want to add it directly to your project, (to debug your tests and so on).

I also found that packages are not always as often as you might want, so it might be because you need a new feature or something added to your test.

Before we start

– I am using Visual Studio 2019, but it should work fine with 2017 and even 2015
– I am using the latest Google Test (v1.10 at the time of writing this, but the principle is the same for almost all versions and should work fine for a few versions.

Getting started

  • Go to Google test on Github and get the latest release, make sure you get the ‘source code’
  • Don’t get it from anywhere else, you don’t know what has been changed and who changed it!
  • Remember that google test is written to a test application, not your main application!
  • Create a new console application, (File > New > Project …) and select “Console App”

Now add Google Test

  • In your project, create a new ‘folder’, I normally prefer to have it mirror what it looks like in my actual windows source code directory, but you could call it whatever you want, (like I did below).
  • Right-click on the ‘src’ folder and select “Add > Existing Items…”
  • Navigate to the folder you extracted, and find the ‘googletest > src’ folder
  • And then select ‘gtest_main.cc’ and ‘gtest-all.cc’, nothing else.
  • Finally, right-click your project and select “properties” and edit the “Additional Include Directories”
  • Add the directory relative to the location of your solution
    You could enter the full path if you want, (but that … blah), or you could use macros …
    Either way, make sure that the path is valid.
  • Make sure that the changes you make apply to all your build configuration.
    In the screenshot above I have x64 and Release but make sure that you choose all the configurations you will be using.
  • Compile your code … nothing funny should happen, (’cause you did nothing really).
    If you get an error, double check your paths and permissions.

Write and run a test

Last but not least, write a very simple test in your main and run a test

You do the rest

Now that a simple test is running, you can start creating tests for your own project.

#include <gtest/gtest.h>

TEST(EngineString, Basic) {
  const auto data = "abcdefg";
  ASSERT_EQ(data, u"abcdefg");
}

int main(int argc, char **argv) {
    ::testing::InitGoogleTest(&argc, argv); 
    return RUN_ALL_TESTS();
}

Example?

Have a look at my directory watcher project and see my own tests.

As an aside, I am alway looking for comments, reviews and so on, please have a look at the project and comment!

Parallel.ForEach Async in C#

September 13th, 2019 Comments off

As mentioned in my previous post, to get a ‘proper’ parallel foreach that is async is a bit of a pain

So the solution is to write a true async function

public static async Task ForEach<T>(ICollection<T> source, Func<T, Task> body, CancellationToken token )
{
  // create the list of tasks we will be running
  var tasks = new List<Task>(source.Count);
  try
  {
    // and add them all at once.
    tasks.AddRange(source.Select(s => Task.Run(() => body(s), token)));

    // execute it all with a delay to throw.
    for (; ; )
    {
      // very short delay
      var delay = Task.Delay(1, token );

      // and all our tasks
      await Task.WhenAny( Task.WhenAll(tasks), delay).ConfigureAwait(false);
      if (tasks.All(t => t.IsCompleted))
      {
        break;
      }
      
      //
      // ... use a spinner or something
    }
    await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);

    // throw if we are done here.
    token.ThrowIfCancellationRequested();
  }
  finally
  {
    // find the error(s) that might have happened.
    var errors = tasks.Where(tt => tt.IsFaulted).Select(tu => tu.Exception).ToList();

    // we are back in our own thread
    if (errors.Count > 0)
    {
      throw new AggregateException(errors);
    }
  }
}

And you can call it …

await ParallelAsync.ForEach(number, async (numbers) =>
{
  // blah ... 
  
  // blah ....
  await DoSomethingAmazing( number ).ConfigureAwait(false);
}, CancellationToken.None).ConfigureAwait( false );

Of course, you can refine it by adding check for tokens that cannot be cancelled as well as empty sources

First prize you must make sure that the body of the ForEach takes in the token and cancels cleanly otherwise this will jump out with thread left up in the air… but at least it will get out.

Edit: As someone pointed out to me on StackOverflow there are a couple of subtle ways I can improve my implementation … so I added them here

Categories: development Tags: , , ,