JoaoBapt
JoaoBapt

Reputation: 195

Check presence of custom overload of function when template overload is available

I am designing an utility header that pumps binary data off an sf::InputStream. For ease of use, is comprises a single function name, readFromStream, that has a lot of (templated and non-templated) overloads for automatically deserializing standard-layout types and type compounds like vectors, tuples and my custom-designed grid class. The complete implementation can be found here: https://github.com/JoaoBaptMG/ReboundTheGame/blob/master/MainGame/utility/streamCommons.hpp

So, I have defined an overload readFromStream that pumps out a vector of any type by calling readFromStream again recursively:

template <typename T, typename std::enable_if<!is_optimization_viable<T>::value, int>::type = 0>
bool readFromStream(sf::InputStream &stream, std::vector<T> &value)
{
    size_t size;

    if (!readFromStream(stream, VarLength(size)))
        return false;

    std::vector<T> newVal(size, T());
    for (auto &val : newVal)
        if (!readFromStream(stream, val))
            return false;

    newVal.swap(value);
    return true;
}

I'd like to write an optimized version for standard-layout classes for that there's not an overload for readFromStream, so we can exploit the memory layout of them and blit them in a single read call:

// trait is_optimization_viable is what I'm having trouble to write
template <typename T, typename std::enable_if<is_optimization_viable<T>::value, int>::type = 0>
bool readFromStream(sf::InputStream &stream, std::vector<T> &value)
{
    size_t size;

    if (!readFromStream(stream, VarLength(size)))
        return false;

    std::vector<T> newVal(size, T());
    if (stream.read(newVal.data(), size*sizeof(T)) != size*sizeof(T))
        return false;

    newVal.swap(value);
    return true;
}

Well, I could use a solution described on other answers to detect presence of a function, but there's a catch. When the type is standard-layout, I have a default readFromStream that reads like this:

template <typename T, typename std::enable_if<std::is_standard_layout<T>::value, int>::type = 0>
bool readFromStream(sf::InputStream &stream, T& value)
{
    return stream.read((void*)&value, sizeof(T)) == sizeof(T);
}

So, there's always a function that does the serialization, not just the one I wanted. The problem I want to solve here is: how can I detect the presence of a non-default readFromString for type T, in order to disable the optimized version of readFromString for std::vector<T>?

I have tried to pull a few tricks. I can't limit the optimization to POD types because I'm using sf::Vector2<T> on some types I want to deserialize, which is not POD. I tried to compare the function addresses I get when I use a non-templatized and templatized function, like:

using FPtr = bool(*)(sf::InputStream&, T&);
return (FPtr)readFromStream == (FPtr)readFromStream<T>;

But, strangely enough, it didn't work. And I researched a lot of solutions, but none I could adapt to what I needed. Maybe it's not possible in C++, and I'll have to resort "marking" the types I don't want to be optimized. Or maybe it's some obscure template I haven't thought of. How could I do this?

Upvotes: 1

Views: 316

Answers (1)

Mike Kinghan
Mike Kinghan

Reputation: 61232

As I understand it your problem is:

is_optimization_viable<T>;

could be defined by:

template<typename T>
using is_optimization_viable<T> = std::is_standard_layout<T>;

but for the fact that, for certain values of T that are standard layout you nonetheless require a custom bool readFromStream(sf::InputStream &stream, T &value), overload which means they are not optimization-viable.

Well as you must write these custom overloads, you know what those exceptional values of T are. Say they are types X, Y, Z. Then you can define the trait as:

#include <type_traits>

template<typename T, typename ...Us>
struct is_one_of;

template<typename T>
struct is_one_of<T> {
    static constexpr bool value = false;
};

template<typename T, typename First, typename ...Rest>
struct is_one_of<T,First,Rest...> {
    static constexpr bool value =
        std::is_same<T,First>::value || is_one_of<T,Rest...>::value;
};

// ^ C++17: `std::disjunction` does the job

template<typename T>
using has_custom_read_from_stream = is_one_of<T,X,Y,Z>;

template<typename T>
struct is_optimization_viable {

    static constexpr bool value = std::is_standard_layout<T>::value &&
        !has_custom_read_from_stream<T>::value;
};

I appreciate that you'd rather avoid the ongoing maintenance of the hard-coded type-list X, Y, Z, and prefer somehow to SFINAE-probe whether a call readFromStream(s, t) will be a call to one of the custom overloads for some std::declval-ed s and t.

But that's a mirage. You tell us, there will be some overload readFromStream(s, t) that will compile whatever the type of t. If so, a SFINAE probe will always tell you that Yes, readFromStream(s, t) will compile - for any T as the unqualified type of t. And you still have to make a compiletime decision as to whether T is one of the custom types, and if not, whether it is standard-layout.

That's all there is to the problem. To tell whether T is one of the custom types you must either test it for identity with any one of them disjunctively, as shown, or your must find a trait independent of their identities that is satisfied by all and only the custom types. As you don't tell us what those custom types are, I can't suggest any such trait, but if you find one then it will define or replace has_custom_read_from_stream<T>.

Incidentally, I second @NirFriedman's comment: is std::standard_layout really what you mean?

Upvotes: 1

Related Questions