diff --git a/.gitignore b/.gitignore
index 4a8fa099f9fcfd8230b1c9bf82c12c7c5d53ca46..aabcf599eafcc64dae2b401ca390522780742104 100644
--- a/.gitignore
+++ b/.gitignore
@@ -290,3 +290,7 @@ pyrightconfig.json
 
 # Hidden files
 .*
+
+# Downloaded meson subprojects
+subprojects/*
+!subprojects/*.wrap
diff --git a/meson.build b/meson.build
index e6d4f65bfb2fe773b8030bed43d8270d201c3007..d68803dc7128f061393141d52315e013e5480e3c 100644
--- a/meson.build
+++ b/meson.build
@@ -1,6 +1,7 @@
 project(
 	'praktikum',
 	'c',
+	'cpp',
 	'fortran',
 	default_options: [
 		'warning_level=3',
diff --git a/src/benchmarks/zellularautomat/cpp/main.cpp b/src/benchmarks/zellularautomat/cpp/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b7dc60c71f224543a1b46242aa0579639005d2e4
--- /dev/null
+++ b/src/benchmarks/zellularautomat/cpp/main.cpp
@@ -0,0 +1,164 @@
+/*
+ * simulate a cellular automaton with periodic boundaries (torus-like)
+ * OpenMP version
+ *
+ * (c) 2024 Dorian Stoll (C++ / Eigen port)
+ * (c) 2016 Steffen Christgau (C99 port, modularization, parallelization)
+ * (c) 1996,1997 Peter Sanders, Ingo Boesnach (original source)
+ *
+ * command line arguments:
+ * #1: Number of lines
+ * #2: Number of iterations to be simulated
+ *
+ */
+
+/*
+ * Eigen defaults to using column-major order for its container types.
+ *
+ * However, using it like that would make the lines parameter correspond to Eigens .cols() function
+ * instead of the more fitting .rows(). To make the code more obvious, we change the default.
+ */
+#define EIGEN_DEFAULT_TO_ROW_MAJOR
+
+#include <Eigen/Eigen>
+
+#include <cassert>
+#include <cstdint>
+#include <cstdlib>
+#include <iostream>
+#include <stdexcept>
+
+class Automat {
+private:
+	/* "ADT" State */
+	using cell_state_t = uint8_t;
+
+	using Field = Eigen::Matrix<cell_state_t, Eigen::Dynamic, Eigen::Dynamic>;
+
+private:
+	/* horizontal size of the configuration */
+	static constexpr Eigen::Index XSIZE = 1024;
+	static constexpr Eigen::Index LINE_SIZE = XSIZE + 2;
+
+	/* annealing rule from ChoDro96 page 34
+	 * the table is used to map the number of nonzero
+	 * states in the neighborhood to the new state
+	 */
+	static constexpr std::array<cell_state_t, 10> ANNEAL {0, 0, 0, 0, 1, 0, 1, 1, 1, 1};
+
+private:
+	int m_lines;
+
+	Field m_from {};
+	Field m_to {};
+
+public:
+	Automat(const int lines) : m_lines {lines}
+	{
+		if (lines <= 0)
+			throw std::invalid_argument {"There must be at least one line"};
+
+		m_from.resize(lines + 2, LINE_SIZE);
+		m_to.resize(lines + 2, LINE_SIZE);
+	}
+
+	void run(const int its)
+	{
+		this->init_config();
+
+		for (int i = 0; i < its; i++) {
+			this->boundary();
+			this->simulate();
+
+			std::swap(m_from, m_to);
+		}
+	}
+
+	void report()
+	{
+		auto block = m_from(Eigen::seqN(1, m_lines), Eigen::all);
+
+		for (Eigen::Index y = 0; y < block.rows(); y++) {
+			block(y, 0) = 0;
+			block(y, block.cols() - 1) = 0;
+		}
+
+		const auto start = block.data();
+		const auto end = start + (block.size() * sizeof(*start));
+
+		std::copy(start, end, std::ostreambuf_iterator(std::cout));
+	}
+
+private:
+	/* random starting configuration */
+	void init_config()
+	{
+		srand(424243);
+
+		for (Eigen::Index y = 1; y <= m_to.rows() - 2; y++) {
+			for (Eigen::Index x = 1; x <= m_from.cols() - 2; x++)
+				m_from(y, x) = rand() % 2;
+		}
+	}
+
+	/* treat torus like boundary conditions */
+	void boundary()
+	{
+#pragma omp sections
+		{
+#pragma omp section
+#pragma omp parallel for
+			for (Eigen::Index y = 0; y < m_from.rows(); y++) {
+				/* copy rightmost column to the buffer column 0 */
+				m_from(y, 0) = m_from(y, m_from.cols() - 2);
+
+				/* copy leftmost column to the buffer column XSIZE + 1 */
+				m_from(y, m_from.cols() - 1) = m_from(y, 1);
+			}
+
+#pragma omp section
+#pragma omp parallel for
+			for (Eigen::Index x = 0; x < m_from.cols(); x++) {
+				/* copy bottommost row to buffer row 0 */
+				m_from(0, x) = m_from(m_from.rows() - 2, x);
+
+				/* copy topmost row to buffer row lines + 1 */
+				m_from(m_from.rows() - 1, x) = m_from(1, x);
+			}
+		}
+	}
+
+	/* make one simulation iteration with lines lines.
+	 * old configuration is in from, new one is written to to.
+	 */
+	void simulate()
+	{
+#pragma omp parallel for
+		for (Eigen::Index y = 1; y <= m_from.rows() - 2; y++) {
+			for (Eigen::Index x = 1; x <= m_from.cols() - 2; x++)
+				m_to(y, x) = transition(m_from, x, y);
+		}
+	}
+
+	/* a: reference to matrix; x,y: coordinates; result: n-th element of anneal,
+	   where n is the number of neighbors */
+	cell_state_t transition(const Field &a, const Eigen::Index x, const Eigen::Index y) const
+	{
+		return ANNEAL[a(Eigen::seq(y - 1, y + 1), Eigen::seq(x - 1, x + 1)).sum()];
+	}
+};
+
+int main(int argc, char **argv)
+{
+	assert(argc == 3);
+
+	int lines = atoi(argv[1]);
+	int its = atoi(argv[2]);
+
+	Automat ca {lines};
+
+	ca.run(its);
+	ca.report();
+
+	return EXIT_SUCCESS;
+}
diff --git a/src/benchmarks/zellularautomat/cpp/meson.build b/src/benchmarks/zellularautomat/cpp/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..772dffb91af86180f6ac3e5cb336ab38e5808524
--- /dev/null
+++ b/src/benchmarks/zellularautomat/cpp/meson.build
@@ -0,0 +1,31 @@
+sources = [
+	'main.cpp',
+]
+
+# Build wrapped dependencies as static libraries and disable warnings
+dependency_options = [
+	'default_library=static',
+	'warning_level=0',
+	'werror=false',
+]
+
+dependencies = [
+	dependency('openmp'),
+	dependency(
+		'eigen3',
+		fallback: ['eigen', 'eigen_dep'],
+		default_options: dependency_options,
+		include_type: 'system',
+	),
+]
+
+options = [
+	'cpp_std=c++17',
+]
+
+executable(
+	'zellularautomat-cpp',
+	sources,
+	dependencies: dependencies,
+	override_options: options,
+)
diff --git a/src/benchmarks/zellularautomat/meson.build b/src/benchmarks/zellularautomat/meson.build
index c957e89f62d3a1a6f452b225bafc3e970cb764f0..3a509a0d52d0c0724ee0b2180e39f37c8663d6c8 100644
--- a/src/benchmarks/zellularautomat/meson.build
+++ b/src/benchmarks/zellularautomat/meson.build
@@ -1,2 +1,3 @@
 subdir('c')
+subdir('cpp')
 subdir('fortran')
diff --git a/subprojects/eigen.wrap b/subprojects/eigen.wrap
new file mode 100644
index 0000000000000000000000000000000000000000..becc4767c7603d3ae5d6a0b2ea6437ee5857fde1
--- /dev/null
+++ b/subprojects/eigen.wrap
@@ -0,0 +1,13 @@
+[wrap-file]
+directory = eigen-3.4.0
+source_url = https://gitlab.com/libeigen/eigen/-/archive/3.4.0/eigen-3.4.0.tar.bz2
+source_filename = eigen-3.4.0.tar.bz2
+source_hash = b4c198460eba6f28d34894e3a5710998818515104d6e74e5cc331ce31e46e626
+patch_filename = eigen_3.4.0-2_patch.zip
+patch_url = https://wrapdb.mesonbuild.com/v2/eigen_3.4.0-2/get_patch
+patch_hash = cb764fd9fec02d94aaa2ec673d473793c0d05da4f4154c142f76ef923ea68178
+source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/eigen_3.4.0-2/eigen-3.4.0.tar.bz2
+wrapdb_version = 3.4.0-2
+
+[provide]
+eigen3 = eigen_dep