diff --git a/include/libcamera/base/utils.h b/include/libcamera/base/utils.h
index d32bd1cd62e0..555da71f124b 100644
--- a/include/libcamera/base/utils.h
+++ b/include/libcamera/base/utils.h
@@ -78,67 +78,23 @@ struct timespec duration_to_timespec(const duration &value);
 std::string time_point_to_string(const time_point &time);
 
 #ifndef __DOXYGEN__
-struct _hex {
+struct hex {
 	uint64_t v;
 	unsigned int w;
+
+	template<typename T, std::enable_if_t<std::is_integral_v<T>> * = nullptr>
+	hex(T value, unsigned int width = sizeof(T) * 2)
+		: v(static_cast<std::make_unsigned_t<T>>(value)),
+		  w(width)
+	{
+	}
 };
 
 std::basic_ostream<char, std::char_traits<char>> &
-operator<<(std::basic_ostream<char, std::char_traits<char>> &stream, const _hex &h);
-#endif
-
-template<typename T,
-	 std::enable_if_t<std::is_integral<T>::value> * = nullptr>
-_hex hex(T value, unsigned int width = 0);
-
-#ifndef __DOXYGEN__
-template<>
-inline _hex hex<int8_t>(int8_t value, unsigned int width)
-{
-	return { static_cast<uint64_t>(value), width ? width : 2 };
-}
-
-template<>
-inline _hex hex<uint8_t>(uint8_t value, unsigned int width)
-{
-	return { static_cast<uint64_t>(value), width ? width : 2 };
-}
-
-template<>
-inline _hex hex<int16_t>(int16_t value, unsigned int width)
-{
-	return { static_cast<uint64_t>(value), width ? width : 4 };
-}
-
-template<>
-inline _hex hex<uint16_t>(uint16_t value, unsigned int width)
-{
-	return { static_cast<uint64_t>(value), width ? width : 4 };
-}
-
-template<>
-inline _hex hex<int32_t>(int32_t value, unsigned int width)
-{
-	return { static_cast<uint64_t>(value), width ? width : 8 };
-}
-
-template<>
-inline _hex hex<uint32_t>(uint32_t value, unsigned int width)
-{
-	return { static_cast<uint64_t>(value), width ? width : 8 };
-}
-
-template<>
-inline _hex hex<int64_t>(int64_t value, unsigned int width)
-{
-	return { static_cast<uint64_t>(value), width ? width : 16 };
-}
-
-template<>
-inline _hex hex<uint64_t>(uint64_t value, unsigned int width)
-{
-	return { static_cast<uint64_t>(value), width ? width : 16 };
-}
+operator<<(std::basic_ostream<char, std::char_traits<char>> &stream, const hex &h);
+#else
+template<typename T, std::enable_if_t<std::is_integral_v<T>> * = nullptr>
+void hex(T value, unsigned int width = 0);
 #endif
 
 size_t strlcpy(char *dst, const char *src, size_t size);
diff --git a/src/libcamera/base/utils.cpp b/src/libcamera/base/utils.cpp
index cb9fe0049c83..446c9a05e96d 100644
--- a/src/libcamera/base/utils.cpp
+++ b/src/libcamera/base/utils.cpp
@@ -187,7 +187,7 @@ std::string time_point_to_string(const time_point &time)
 }
 
 std::basic_ostream<char, std::char_traits<char>> &
-operator<<(std::basic_ostream<char, std::char_traits<char>> &stream, const _hex &h)
+operator<<(std::basic_ostream<char, std::char_traits<char>> &stream, const hex &h)
 {
 	stream << "0x";
 
diff --git a/test/meson.build b/test/meson.build
index 96c4477f04b2..52f04364e4fc 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -73,7 +73,7 @@ internal_tests = [
     {'name': 'timer-fail', 'sources': ['timer-fail.cpp'], 'should_fail': true},
     {'name': 'timer-thread', 'sources': ['timer-thread.cpp']},
     {'name': 'unique-fd', 'sources': ['unique-fd.cpp']},
-    {'name': 'utils', 'sources': ['utils.cpp'], 'should_fail': true},
+    {'name': 'utils', 'sources': ['utils.cpp']},
     {'name': 'vector', 'sources': ['vector.cpp']},
     {'name': 'yaml-parser', 'sources': ['yaml-parser.cpp']},
 ]
