template function overload in non-template class using SFINAE

256 Views Asked by At

TL;DR

A couple of templatized and overloaded non-templatized member functions in a non-templatized class should all end up routing through the same member function to perform the actual work. All the overloads and templatizations are done to convert the "data buffer" into gsl::span<std::byte> type (essentially a close relative to std::array<std::byte, N> from the Guidelines Support Library)


Wall of code

#include <array>
#include <cstdlib>
#include <iostream>
#pragma warning(push)
#pragma warning(disable: 4996)
#include <gsl.h>
#pragma warning(pop)

// convert PoD into "memory buffer" for physical I/O
// ignore endianness and padding/alignments for this example
template<class T> gsl::span<std::byte> byte_span(T& _x) {
    return { reinterpret_cast<std::byte*>(&_x), sizeof(T) };
}

// implementation of physical I/O (not a functor, but tempting)
struct A {
    enum class E1 : uint8_t { v1 = 10, v2, v3, v4 };
    bool f(uint8_t _i1, gsl::span<std::byte> _buf = {});    // a - "in the end" they all call here
    bool f(E1 _i1, gsl::span<std::byte> _buf = {});         // b
    template<typename T, typename = std::enable_if_t< std::is_integral<T>::value > >
        bool f(uint8_t _i1, T& _val);                       // c
    template<typename T, typename = std::enable_if_t< std::is_integral<T>::value > >
        bool f(E1 _i1, T& _val);                            // d
};

bool A::f(uint8_t _i1, gsl::span<std::byte> _buf)
{
    std::cout << "f() uint8_t{" << (int)_i1 << "} with " << _buf.size() << " elements\n";
    return true;
}

bool A::f(E1 _i1, gsl::span<std::byte> _buf)
{
    std::cout << "f() E1{" << (int)_i1 << "} with " << _buf.size() << " elements\n\t";
    return f((uint8_t)_i1, _buf);
}

template<class T, typename>
bool A::f(uint8_t _i1, T& _val)
{
    std::cout << "template uint8_t\n\t";
    return f(_i1, byte_span(_val));
}

template<class T, typename>
bool A::f(E1 _i1, T& _val)
{
    std::cout << "template E1\n\t";
    return f(_i1, byte_span(_val));
}

int main(){
    A a = {};
    std::array<std::byte, 1> buf;
    long i = 2;

    // regular function overloads
    a.f(1, buf);           // should call (a)
    a.f(A::E1::v1, buf);   // should call (b)

    // template overloads
    a.f(2, i);             // should call (c)
    a.f(A::E1::v2, i);     // should call (d)

    struct S { short i; };
    // issue
    //S s;
    //a.f(3, s);             // should call (c)
    //a.f(A::E1::v3, s);     // should call (d)

    //// bonus - should use non-template overrides
    //S sv[2] = {};
    //a.f(5, sv);            // should call (a)
    //a.f(A::E1::v1, sv);    // should call (b)
}

Details

The struct S is a PoD and it is tempting to change the template's enable_if to use the std::is_trivial or std::is_standard_layout. Unfortunately both these solutions "grab too much" and end up matching std::array (even if they do fix the compilation error of the //issue block).

The solution I have right now looks like a dead-end as my gut feeling is to start adding more template parameters and it seems to be getting very hairy very soon :(

Question

My goal is to achieve the following: use the class A's bool f() member function without too much syntactic overhead for any PoD (possibly including C arrays - see "bonus" in code) as shown in the body of main() and no runtime function call overhead for types that are auto-convertible to gsl::span (like std::array and std::vector).

Ideally...

I'd like to have a single templatized function per first parameter (either E1 or uint8_t) with multiple specializations listed outside the class's body to further reduce the perceived code clutter in the class's declaration and I can't figure out the way to properly do that. Something like the following (illegal C++ code below!):

struct A {
// ...
template<typename T> bool f(uint8_t _i1, T& _val);
template<typename T> bool f(E1 _i1, T& _val);
};

template<> bool f<is_PoD<T> && not_like_gsl_span<T>>(uint8_t /*...*/}
template<> bool f<is_PoD<T> && not_like_gsl_span<T>>(E1 /*...*/}
template<> bool f<is_like_gsl_span<T>>(uint8_t /*...*/}
template<> bool f<is_like_gsl_span<T>>(E1 /*...*/}

If this is not achievable I'd like to know why.

I'm on MSVC 2017 with C++17 enabled.

1

There are 1 best solutions below

2
On

The first answer was a little wrong, for some reasons, I was totally confused by gsl. I thought it is something super specific. I am not using Guideline Support Library, though I have seen it and it looks good. Fixed the code to properly work with gsl::span type.

struct A {
  enum class E1 : uint8_t { v1 = 10, v2, v3, v4 };

private:
  template <typename T>
  static auto make_span(T& _x) ->
    typename std::enable_if<std::is_convertible<T&, gsl::span<std::byte>>::value,
                            gsl::span<std::byte>>::type {
    std::cout << "conversion" << std::endl;
    return _x;
  }

  template <typename T>
  static auto make_span(T& _x) ->
    typename std::enable_if<!std::is_convertible<T&, gsl::span<std::byte>>::value,
                            gsl::span<std::byte>>::type {
    std::cout << "cast" << std::endl;
    return {reinterpret_cast<std::byte*>(&_x), sizeof(T)};
  }

public:
  template <typename T, typename U>
  bool f(T _i, U& _buf) {
    static_assert(
      std::is_convertible<U&, gsl::span<std::byte>>::value || std::is_trivial<U>::value,
      "The object must be either convertible to gsl::span<std::byte> or be of a trivial type");

    const auto i = static_cast<uint8_t>(_i);
    const auto span = make_span(_buf);
    std::cout << "f() uint8_t{" << (int)i << "} with " << span.size() << " elements\n";
    return true;
  }
};