0

I'm trying to write a template class that will wrap a type that could be either a primitive (uint8_t up to float) or a char* string. If the class wraps a primitive type, setting its value to any primitive will do a straight cast, while setting a char* will to an ascii to primitive conversion (atoi, sprintf, etc). If it wraps a char*, the class should manage an internal char* buffer and convert primitive to char*.

If I limit my example to only uint32_t values, this is the template that would deal with any primitive type (with obvious error checking details omitted):

template<typename T>
class Measurement
{
public:
    Measurement();
    ~Measurement();

    void setU32Value(uint32_t value);
    void setStringValue(const char* std);

    uint32_t asU32() const;
    const char* asString() const;

private:
    T _value;
};

template<typename T>
void Measurement<T>::setU32Value(uint32_t value)
{
    _value = (T)value;
}

template<typename T>
void Measurement<T>::setStringValue(const char* value)
{
    _value = (T)::atoi(value);
}

template<typename T>
uint32_t Measurement<T>::asU32() const
{
    return (uint32_t)_value;
}

template<typename T>
const char* Measurement<T>::asString() const
{
    ostringstream oss;
    oss << _value;
    return oss.str();
}

But it won't work if I try to Measurement<char*> because it won't convert setU32Value wont' convert "value" to a string representation and the asU32() won't convert it back.

Can anyone suggest a way to make this work?

Thanks.

DaveR
  • 1,295
  • 1
  • 13
  • 35
  • You might specialize `Measurement`, but how do you want to handle ownership with conversion? using `std::string` seems more appropriate in that case. – Jarod42 Jul 16 '20 at 17:32
  • My recommendation is that you store the data as plain strings (i.e. `std::string`) and use conversion functions for anything that isn't a string. – Some programmer dude Jul 16 '20 at 17:32
  • 1
    Avoid C-cast, prefer `static_cast`. – Jarod42 Jul 16 '20 at 17:32
  • You cannot have T = const anything because then _value must be set at construction time. It cannot be set by a function later. – stackoverblown Jul 16 '20 at 17:40
  • @Jarod42 using string won't solve the problem Measurement would result in asU32() {return (uint32_t)_value;}, which won't compile. – DaveR Jul 16 '20 at 17:54
  • Storing an std::string is expensive, as is doing constant back-and-forth conversion. – DaveR Jul 16 '20 at 17:58
  • @bolov how would a variant help? How would that solve the assignment issue? – DaveR Jul 16 '20 at 19:37

1 Answers1

1

Your class definitely should not manually manage the lifetime of its string. Please see the Single-responsibility principle, The rule of three/five/zero and RAII. So the string storage type should be std::string and not char*.

C++20 concepts:

#include <string>
#include <concepts>
#include <type_traits>

template<typename T>
class Measurement
{
private:
    T _value{};

public:
    Measurement() = default;

    template <std::integral U> requires std::integral<T>
    void setIntValue(U value)
    {
        _value = static_cast<U>(value);
    }
    
    template <std::integral U> requires std::same_as<T, std::string>
    void setIntValue(U value)
    {
        _value = std::to_string(value);
    }

    void setStringValue(const std::string& str) requires std::integral<T>
    {
        _value = static_cast<T>(std::stoll(str));
    }

    void setStringValue(std::string str) requires std::same_as<T, std::string>
    {
        _value = std::move(str);
    }

    template <std::integral U> requires std::integral<T>
    U asInt() const
    {
        return static_cast<U>(_value);
    }

    template <std::integral U> requires std::same_as<T, std::string>
    U asInt() const
    {
        return static_cast<U>(std::stoll(_value));
    }

    std::string asString() const requires std::integral<T>
    {
        return std::to_string(_value);
    }

    std::string asString() const requires std::same_as<T, std::string>
    {
        return _value;
    }
};

Usage example:

auto test()
{
    {
        auto m = Measurement<long>{};
        m.setIntValue<int>(24);
    
        int a = m.asInt<int>();
        std::string s = m.asString();

        m.setStringValue("11");

        a = m.asInt<int>();
        s = m.asString();
    }


    {
        auto m = Measurement<std::string>{};
        m.setIntValue<int>(24);
    
        int a = m.asInt<int>();
        std::string s = m.asString();

        m.setStringValue("11");

        a = m.asInt<int>();
        s = m.asString();
    }
}

Template specialization

#include <string>
#include <type_traits>

template <class T, class Enable = void> class Measurement;

template <class T>
class Measurement<T, std::enable_if_t<std::is_integral_v<T>>>
{
private:
    T _value{};

public:
    Measurement() = default;

    template <std::integral U>
    void setIntValue(U value)
    {
        _value = static_cast<U>(value);
    }
    
    void setStringValue(const std::string& str)
    {
        _value = static_cast<T>(std::stoll(str));
    }

    template <std::integral U>
    U asInt() const
    {
        return static_cast<U>(_value);
    }

    std::string asString() const requires std::integral<T>
    {
        return std::to_string(_value);
    }
};

template <>
class Measurement<std::string>
{
private:
    std::string _value{};

public:
    Measurement() = default;

    
    template <std::integral U>
    void setIntValue(U value)
    {
        _value = std::to_string(value);
    }

    void setStringValue(std::string str)
    {
        _value = std::move(str);
    }

    template <std::integral U>
    U asInt() const
    {
        return static_cast<U>(std::stoll(_value));
    }

    std::string asString() const
    {
        return _value;
    }
};

Same usage as before.


I have to say that the class seems too bloated. I would instead have a class for storing the measurement and a separate utility to help convert between measurement and integers/string.

bolov
  • 72,283
  • 15
  • 145
  • 224