diff --git a/Documentation/Doxyfile.in b/Documentation/Doxyfile.in
index 8e6fbdbb92b6..6de59d407e7b 100644
--- a/Documentation/Doxyfile.in
+++ b/Documentation/Doxyfile.in
@@ -877,6 +877,7 @@ EXCLUDE_SYMBOLS        = libcamera::BoundMethodArgs \
                          libcamera::BoundMethodPackBase \
                          libcamera::BoundMethodStatic \
                          libcamera::SignalBase \
+                         *::details::* \
                          std::*
 
 # The EXAMPLE_PATH tag can be used to specify one or more files or directories
diff --git a/src/libcamera/include/utils.h b/src/libcamera/include/utils.h
index e467eb21c518..6e9b9259456a 100644
--- a/src/libcamera/include/utils.h
+++ b/src/libcamera/include/utils.h
@@ -8,12 +8,15 @@
 #define __LIBCAMERA_UTILS_H__
 
 #include <algorithm>
+#include <array>
 #include <chrono>
+#include <functional>
 #include <memory>
 #include <ostream>
 #include <string>
 #include <string.h>
 #include <sys/time.h>
+#include <type_traits>
 
 #define ARRAY_SIZE(a)	(sizeof(a) / sizeof(a[0]))
 
@@ -108,6 +111,45 @@ inline _hex hex<uint64_t>(uint64_t value, unsigned int width)
 
 size_t strlcpy(char *dst, const char *src, size_t size);
 
+namespace details {
+
+namespace make_array {
+
+	template<class B>
+	struct negation : std::integral_constant<bool, !bool(B::value)> {};
+
+	template<class...> struct conjunction : std::true_type {};
+	template<class B1> struct conjunction<B1> : B1 {};
+	template<class B1, class... Bn>
+	struct conjunction<B1, Bn...>
+		: std::conditional_t<bool(B1::value), conjunction<Bn...>, B1> {};
+
+	template<class> struct is_ref_wrapper : std::false_type {};
+	template<class T> struct is_ref_wrapper<std::reference_wrapper<T>> : std::true_type {};
+
+	template<class D, class...>
+	struct return_type_helper {
+		using type = D;
+	};
+	template <class... Types>
+	struct return_type_helper<void, Types...> : std::common_type<Types...> {
+		static_assert(conjunction<negation<is_ref_wrapper<std::decay_t<Types>>>...>::value,
+			      "Types cannot contain reference_wrappers when D is void");
+	};
+
+	template<class D, class... Types>
+	using return_type = std::array<typename return_type_helper<D, Types...>::type,
+				       sizeof...(Types)>;
+} /* namespace make_array */
+
+} /* namespace details */
+
+template<class D = void, class... Types>
+constexpr details::make_array::return_type<D, Types...> make_array(Types&&... t)
+{
+	return {std::forward<Types>(t)... };
+}
+
 } /* namespace utils */
 
 } /* namespace libcamera */
diff --git a/src/libcamera/utils.cpp b/src/libcamera/utils.cpp
index 4beffdab5eb6..b639cfa83d0c 100644
--- a/src/libcamera/utils.cpp
+++ b/src/libcamera/utils.cpp
@@ -199,6 +199,20 @@ size_t strlcpy(char *dst, const char *src, size_t size)
 	return strlen(src);
 }
 
+/**
+ * \fn template<class D, class... Types> libcamera::utils::make_array(Types&&... t)
+ * \brief Create a std::array with automatic deduction of element type and count
+ * \param[in] t The initialization values for the array elements
+ *
+ * This function creates and returns an instance of std::array whose size is
+ * equal to the number of arguments. If the template argument \a D is void, the
+ * array element type is deduced from the elements through
+ * std::common_types_t<Types...>. Otherwise it is set to D. The elements are
+ * initialized from the corresponding arguments.
+ *
+ * \return A std::array initialized from the arguments
+ */
+
 } /* namespace utils */
 
 } /* namespace libcamera */
