InputStream::readType <> template specializations


#1

It would be great if we had this:

template <class T>
T InputStream::readType ();

These would be inline specializations that call the existing virtuals. For example:

template <>
T InputStream::readType <char> () { return this->readByte(); }

Also we would want the BigEndian versions too.

This would come in very handy if you have a typedef corresponding to the type of value you want to read, especially when it is a template parameter in your own class.

I can add this, Jules do you want it?


#2

(Pretty sure I once tried to do that, but had compiler issues. That was probably back in the days of VC6 though!)

Yes, it’s a nice idea, will have a look at that!


#3

Great! Well, just remember that every point of call will have to explicitly mention the type. Like:

FileOffset offset = stream->readType <FileOffset> ();

But that’s kind of the point.


#4

You know - I think this will break in VC2005, which unfortunately people still use… IIRC it can’t handle template specialisation for member functions (?)


#5

You could #ifdef it so the member functions are only available for later versions.

And/or you can provide a flat specialized template function:

template <class T>
T readTypeFromInputStream (InputStream& stream);

#6

I just wasted a few hours getting bit by a bug.

I have this line:

keyRecord->valSize = stream.readIntBigEndian ();

Where valSize is std::size_t. Works great in Windows, but on my Ubuntu system std::size_t is a 64 bit unsigned value and this blows up, in a subtle way. If we had the template specialization as I described in my post this wouldn’t have happened. And if I explicitly provided the template argument, the compiler would have warned me about differently sized integer assignment.

Jules! juce_core is starting to show signs of disrepair! This is exactly why I made beast a hard fork. I can’t wait for these fixes!


#7

Well it depends, if you plan to have your stream cross platform I strongly suggest you to use int32_t and stuff like that.


#8

Is VC2005 still relevant nowadays? I think it could be time to finally make this addition :slight_smile:

In the meantime, here’s my utility class with templated read/write functions for everyone to use:

class IOUtils {
public:
	template<typename T>
	static bool write(juce::OutputStream &out, T value);

	template<typename T>
	static T read(juce::InputStream &in);
};

template<>
inline bool IOUtils::write(juce::OutputStream &out, char value) {
	return out.writeByte(value);
}

template<>
inline bool IOUtils::write(juce::OutputStream &out, bool value) {
	return out.writeBool(value);
}

template<>
inline bool IOUtils::write(juce::OutputStream &out, short value) {
	return out.writeShort(value);
}

template<>
inline bool IOUtils::write(juce::OutputStream &out, float value) {
	return out.writeFloat(value);
}

template<>
inline bool IOUtils::write(juce::OutputStream &out, double value) {
	return out.writeDouble(value);
}

template<>
inline bool IOUtils::write(juce::OutputStream &out, int value) {
	return out.writeInt(value);
}

template<>
inline bool IOUtils::write(juce::OutputStream &out, juce::int64 value) {
	return out.writeInt64(value);
}

template<>
inline bool IOUtils::write(juce::OutputStream &out, juce::String value) {
	return out.writeString(value);
}

template<>
inline char IOUtils::read(juce::InputStream &in) {
	return in.readByte();
}

template<>
inline short IOUtils::read(juce::InputStream &in) {
	return in.readShort();
}

template<>
inline float IOUtils::read(juce::InputStream &in) {
	return in.readFloat();
}

template<>
inline double IOUtils::read(juce::InputStream &in) {
	return in.readDouble();
}

template<>
inline int IOUtils::read(juce::InputStream &in) {
	return in.readInt();
}

template<>
inline juce::int64 IOUtils::read(juce::InputStream &in) {
	return in.readInt64();
}

template<>
inline juce::String IOUtils::read(juce::InputStream &in) {
	return in.readString();
}

#9
namespace juce
{
    struct InputStream
    {
        char readByte() { return {1}; }
        short readShort() { return {2}; }
        float readFloat() { return {3}; }
        double readDouble() { return {4}; }
        int readInt() { return {5}; }
    };
}

class IOUtils {
public:
    template<typename T>
    static T read(juce::InputStream &in)
    {
        if constexpr( std::is_same<T, char>::value ) { return in.readByte(); }
        if constexpr( std::is_same<T, short>::value ) { return in.readShort(); }
        if constexpr( std::is_same<T, float>::value ) { return in.readFloat(); }
        if constexpr( std::is_same<T, double>::value ) { return in.readDouble(); }
        if constexpr( std::is_same<T, int>::value ) { return in.readInt(); }
        return 0;
    }
};

:slight_smile:


#10

I wanted my code to specifically fail compilation instead of (silently or not) just not doing anything when a given T is unsupported.

Nice use of std::is_same, though!


#11

maybe you could add a bunch of static_assert()'s for each T that you’re going to handle?


#12

At that point, is it actually simpler to have a bunch of if/else statements in addition to type-checking as opposed to just using template specialization like I did?


#13

I think the template specialisation would be resolved at compile time, vs. an if/else would be executed at run time?
If you are very lucky, the optimiser might understand, that it always uses the same path with a given type, but I wouldn’t bet on that…


#14

That’s precisely the point I was making - I chose template specialization on purpose over a bunch of if/else statements.


#15

if constexpr is evaluated at compile time, so the if-else issues are null


#16

you might consider

    template<typename T>
    static T read(juce::InputStream &in)
    {
        // [...]
        return {};
    ]

instead, it might not be possible to implicitly cast 0 to T…


#17
class IOUtils {
public:
    template<typename T>
    static T read(juce::InputStream &in)
    {
        static_assert(std::is_same<T, char>::value || 
                      std::is_same<T, short>::value ||
                      std::is_same<T, float>::value ||
                      std::is_same<T, double>::value ||
                      std::is_same<T, int>::value,
                      "T is not a readable type!" );
        if constexpr( std::is_same<T, char>::value ) { return in.readByte(); }
        if constexpr( std::is_same<T, short>::value ) { return in.readShort(); }
        if constexpr( std::is_same<T, float>::value ) { return in.readFloat(); }
        if constexpr( std::is_same<T, double>::value ) { return in.readDouble(); }
        if constexpr( std::is_same<T, int>::value ) { return in.readInt(); }
        //etc for all InputStream::read___ functions
        /*
        this should never be hit because the static_assert will fail for any 
        T that InputStream can't read, but it silences the compiler warning 
        that this function doesn't have a return value
        */
        return {}; //
    }
};

#18

Why not just do an if-else chain for the is_same tests and a final else that static_asserts?


#19

static_asserts don’t work like that, apparently. It was discussed in the C++Help discord.

the static_assert is evaluated before the if constexpr

int main()
{
    if constexpr (false) {
        static_assert(false);
    }
    
    std::cout << "Hello, Wandbox!" << std::endl;
}

#20

ok, got a rad solution from the folks in C++Help(discord) that uses c++17 fold expressions:

template<typename... Types>
struct type_list {
    template<typename T>
    constexpr static bool contains = (... || std::is_same_v<T, Types>);
};

class IOUtils {
public:
    template<typename T>
    static T read(juce::InputStream &in)
    {
        static_assert(type_list<char, short, float, double, int>::contains<T>,
                      "T is not a readable type!" );
        
        if constexpr( std::is_same<T, char>::value ) { return in.readByte(); }
        if constexpr( std::is_same<T, short>::value ) { return in.readShort(); }
        if constexpr( std::is_same<T, float>::value ) { return in.readFloat(); }
        if constexpr( std::is_same<T, double>::value ) { return in.readDouble(); }
        if constexpr( std::is_same<T, int>::value ) { return in.readInt(); }
        //etc for every other InputStream::read___
        /*
        this should never be hit because the static_assert will fail for any 
        T that InputStream can't read, but it silences the compiler warning 
        that this function doesn't have a return value
        */
        return {}; 
    }
};

the static_assert will fire if you don’t pass a T that InputStream can deal with, and the if constexpr will force the execution of the type comparison to happen at compile time, not run time, so the complaints about if else branching are nullified. if constexpr(false) doesn’t get compiled, so if your T is a float, the function looks like this:

float read(juce::InputStream& in)
{
   return in.readFloat();
}

I stand corrected, it gets compiled into stuff like this:

    /* First instantiated from: insights.cpp:54 */
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    static inline int read<int>(juce::InputStream & in)
    {
      /* PASSED: static_assert(type_list<char, short, float, double, int>::contains<int>, "T is not a readable type!"); */
      if constexpr(std::integral_constant<bool, 0>::value) ;
      
      if constexpr(std::integral_constant<bool, 0>::value) ;
      
      if constexpr(std::integral_constant<bool, 0>::value) ;
      
      if constexpr(std::integral_constant<bool, 0>::value) ;

      if constexpr(std::integral_constant<bool, 1>::value) {
        return in.readInt();
      }
      
      return {};
    }
    #endif
    
};

you can see more here at cppinsights.io
just paste this in:

#include <iostream>
#include <cstdlib>
#include <type_traits>

namespace juce
{
    struct InputStream
    {
        char readByte() { return {1}; }
        short readShort() { return {2}; }
        float readFloat() { return {3}; }
        double readDouble() { return {4}; }
        int readInt() { return {5}; }
    };
}

template<typename... Types>
struct type_list {
    template<typename T>
    constexpr static bool contains = (... || std::is_same_v<T, Types>);
};

class IOUtils {
public:
    template<typename T>
    static T read(juce::InputStream &in)
    {
        static_assert(type_list<char, short, float, double, int>::contains<T>,
                      "T is not a readable type!" );
        
        if constexpr( std::is_same<T, char>::value ) { return in.readByte(); }
        if constexpr( std::is_same<T, short>::value ) { return in.readShort(); }
        if constexpr( std::is_same<T, float>::value ) { return in.readFloat(); }
        if constexpr( std::is_same<T, double>::value ) { return in.readDouble(); }
        if constexpr( std::is_same<T, int>::value ) { return in.readInt(); }
        //etc for every other InputStream::read___
        /*
        this should never be hit because the static_assert will fail for any 
        T that InputStream can't read, but it silences the compiler warning 
        that this function doesn't have a return value
        */
        return {}; //
    }
};

int main()
{
    juce::InputStream is;
    std::cout << (int)IOUtils::read<char>(is) << std::endl; 
    std::cout << IOUtils::read<short>(is) << std::endl; 
    std::cout << IOUtils::read<float>(is) << std::endl; 
    std::cout << IOUtils::read<double>(is) << std::endl; 
    std::cout << IOUtils::read<int>(is) << std::endl; 
    //std::cout << IOUtils::read<unsigned int>(is) << std::endl;
}