diff --git a/include/libcamera/base/utils.h b/include/libcamera/base/utils.h
index 7083b7ce9ce9..62c7f89c25a1 100644
--- a/include/libcamera/base/utils.h
+++ b/include/libcamera/base/utils.h
@@ -37,6 +37,13 @@ namespace libcamera {
 
 namespace utils {
 
+template<class... Ts>
+struct overloaded : Ts... {
+	using Ts::operator()...;
+};
+template<class... Ts>
+overloaded(Ts...) -> overloaded<Ts...>;
+
 const char *basename(const char *path);
 
 char *secure_getenv(const char *name);
diff --git a/src/libcamera/base/utils.cpp b/src/libcamera/base/utils.cpp
index 42a516097be2..836aec87c539 100644
--- a/src/libcamera/base/utils.cpp
+++ b/src/libcamera/base/utils.cpp
@@ -23,6 +23,41 @@ namespace libcamera {
 
 namespace utils {
 
+/**
+ * \struct overloaded
+ * \brief Helper type for type-matching std::visit implementations
+ * \tparam Ts... Template arguments pack of visitors
+ *
+ * Expand the template argument pack \a Ts... to provide overloaded \a
+ * operator() to support type-matching implementations of the visitor design
+ * pattern using std::visit.
+ *
+ * An example is provided by the STL documentation in the form of:
+ *
+ * \code{.cpp}
+ * template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
+ * template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
+ *
+ * using var_t = std::variant<int, long, double, std::string>;
+ * std::vector<var_t> vec = {10, 15l, 1.5, "hello"};
+ *
+ * for (auto& v: vec) {
+ * 	std::visit(overloaded {
+ * 		[](auto arg) { std::cout << arg << ' '; },
+ * 		[](double arg) { std::cout << std::fixed << arg << ' '; },
+ * 		[](const std::string& arg) { std::cout << std::quoted(arg) << ' '; },
+ * 	}, v);
+ * \endcode
+ *
+ * Use this helper to implement type-matching visitors using std::visit().
+ */
+
+/**
+ * \var <class... Ts> overloaded(Ts...) -> overloaded<Ts...>
+ * \brief Deduction guide necessary for C++17 compatibility
+ * \tparam Ts... Template arguments pack of visitor functions
+ */
+
 /**
  * \brief Strip the directory prefix from the path
  * \param[in] path The path to process
diff --git a/src/libcamera/pipeline/virtual/virtual.cpp b/src/libcamera/pipeline/virtual/virtual.cpp
index efd800ebe3d6..e8ef7e524ccf 100644
--- a/src/libcamera/pipeline/virtual/virtual.cpp
+++ b/src/libcamera/pipeline/virtual/virtual.cpp
@@ -23,6 +23,7 @@
 
 #include <libcamera/base/flags.h>
 #include <libcamera/base/log.h>
+#include <libcamera/base/utils.h>
 
 #include <libcamera/control_ids.h>
 #include <libcamera/controls.h>
@@ -57,13 +58,6 @@ uint64_t currentTimestamp()
 
 } /* namespace */
 
-template<class... Ts>
-struct overloaded : Ts... {
-	using Ts::operator()...;
-};
-template<class... Ts>
-overloaded(Ts...) -> overloaded<Ts...>;
-
 class VirtualCameraConfiguration : public CameraConfiguration
 {
 public:
@@ -428,7 +422,7 @@ bool PipelineHandlerVirtual::initFrameGenerator(Camera *camera)
 {
 	auto data = cameraData(camera);
 	auto &frame = data->config_.frame;
-	std::visit(overloaded{
+	std::visit(utils::overloaded{
 			   [&](TestPattern &testPattern) {
 				   for (auto &streamConfig : data->streamConfigs_) {
 					   if (testPattern == TestPattern::DiagonalLines)
