There's almost no support for unsigned float in hardware so you won't have such off-the-shelf feature but you can still have quite efficient unsigned float by storing the least significant bit in the sign bit. This way you can utilize the available floating-point hardware support instead of writing a software float solution. To do that you can
- manipulate it manually after each operation - This way you need some small correction to the lsb (A.K.A sign bit), for example 1 more long division step, or a 1-bit adder for the addition 
- or by doing the math in higher precision if available - For example for - floatyou can do operations in- doublethen cast back to- floatwhen storing if- sizof(float) < sizeof(double)
 
Here's a simple PoC implementation:
#include <cmath>
#include <cfenv>
#include <bit>
#include <type_traits>
// Does the math in double precision when hardware double is available
#define HAS_NATIVE_DOUBLE
class UFloat
{
public:
    UFloat(double d) : UFloat(0.0f)
    {
        if (d < 0)
            throw std::range_error("Value must be non-negative!");
        uint64_t dbits = std::bit_cast<uint64_t>(d);
        bool lsb = dbits & lsbMask;
        dbits &= ~lsbMask; // turn off the lsb
        d = std::bit_cast<double>(dbits);
        value = lsb ? -(float)d : (float)d;
    }
    UFloat(const UFloat &rhs) : UFloat(rhs.value) {}
    // =========== Operators ===========
    UFloat &operator+=(const UFloat &rhs)
    {
#ifdef HAS_NATIVE_DOUBLE
        // Calculate in higher precision then round back
        setValue((double)value + rhs.value);
#else
        // Calculate the least significant bit manually
        
        bool lhsLsb = std::signbit(value);
        bool rhsLsb = std::signbit(rhs.value);
        // Clear the sign bit to get the higher significant bits
        // then get the sum
        value = std::abs(value);
        value += std::abs(rhs.value);
        if (std::isfinite(value))
        {
            if (lhsLsb ^ rhsLsb) // Only ONE of the 2 least significant bits is 1
            {
                // The sum's lsb is 1, so we'll set its sign bit
                value = -value;
            }
            else if (lhsLsb)
            {
                // BOTH least significant bits are 1s,
                // so we'll add the carry to the next bit
                value = std::nextafter(value, INFINITY);
                // The lsb of the sum is 0, so the sign bit isn't changed
            }
        }
#endif
        return *this;
    }
    UFloat &operator*=(const UFloat &rhs)
    {
#ifdef HAS_NATIVE_DOUBLE
        // Calculate in higher precision then round back
        setValue((double)value * rhs.value);
#else
        // Calculate the least significant bit manually
    
        bool lhsLsb = std::signbit(value);
        bool rhsLsb = std::signbit(rhs.value);
        // Clear the sign bit to get the higher significant bits
        // then get the product
        float lhsMsbs = std::abs(value);
        float rhsMsbs = std::abs(rhs.value);
        // Suppose we have X.xPm with
        //     X: the high significant bits
        //     x: the least significant one
        // and m: the exponent. Same to Y.yPn
        // X.xPm * Y.yPn = (X + 0.x)*2^m * (Y + 0.y)*2^n
        //               = (X + x/2)*2^m * (Y + y/2)*2^n
        //               = (X*Y + X*y/2 + Y*x/2 + x*y/4)*2^(m + n)
        value = lhsMsbs * rhsMsbs; // X*Y
        if (std::isfinite(value))
        {
            uint32_t rhsMsbsBits = std::bit_cast<uint32_t>(rhsMsb);
            value += rhsMsbs*lhsLsb / 2; // X*y/2
            
            uint32_t lhsMsbsBits = std::bit_cast<uint32_t>(lhsMsbs);
            value += lhsMsbs*rhsLsb / 2; // Y*x/2
            
            int lsb = (rhsMsbsBits | lhsMsbsBits) & 1; // the product's lsb
            lsb += lhsLsb & rhsLsb;
            if (lsb & 1)
                value = -value; // set the lsb
            if (lsb > 1)    // carry to the next bit
                value = std::nextafter(value, INFINITY);
        }
#endif
        return *this;
    }
    
    UFloat &operator/=(const UFloat &rhs)
    {
#ifdef HAS_NATIVE_DOUBLE
        // Calculate in higher precision then round back
        setValue((double)value / rhs.value);
#else
        // Calculate the least significant bit manually
        // Do just one more step of long division,
        // since we only have 1 bit left to divide
        throw std::runtime_error("Not Implemented yet!");
#endif
        return *this;
    }
    double getUnsignedValue() const
    {
        if (!std::signbit(value))
        {
            return value;
        }
        else
        {
            double result = std::abs(value);
            uint64_t doubleValue = std::bit_cast<uint64_t>(result);
            doubleValue |= lsbMask; // turn on the least significant bit
            result = std::bit_cast<double>(doubleValue);
            return result;
        }
    }
    
private:
    // The unsigned float value, with the least significant bit (lsb)
    // being stored in the sign bit
    float value;
    
    // the first bit after the normal mantissa bits
    static const uint64_t lsbMask = 1ULL << (DBL_MANT_DIG - FLT_MANT_DIG - 1);
    // =========== Private Constructor ===========
    UFloat(float rhs) : value(rhs)
    {
        std::fesetround(FE_TOWARDZERO); // We'll round the value ourselves
#ifdef HAS_NATIVE_DOUBLE
        static_assert(sizeof(float) < sizeof(double));
#endif
    }
    void setValue(double d)
    {
        // get the bit pattern of the double value
        auto bits = std::bit_cast<std::uint64_t>(d);
        bool lsb = bits & lsbMask;
        // turn off the lsb to avoid rounding when converting to float
        bits &= ~lsbMask;
        d = std::bit_cast<double>(bits);
        value = (float)d;
        if (lsb)
            value = -value;
    }
}
Some more tuning may be necessary to get the correct lsb
Either way you'll need more operations than normal so this may only be good for big arrays where cache footprint is a concern. In that case I suggest to use this as a storage format only, like how FP16 is treated on most current architectures: there are only load/store instructions for it which expands to float or double and convert back. All arithmetic operations are done in float or double only
So the unsigned float should exist in memory only, and will be decoded to the full double on load. This way you work on the native double type and won't need the correction after each operator
Alternatively this can be used with SIMD to operate on multiple unsigned floats at the same time