haael
haael

Reputation: 1047

C++ template specialization - avoid redefinition

I want to have a generic function (or method) that accepts arguments of different types. If the provided type has 'one' method, the function should use it. If it has 'two' method, the function should use it instead.

Here's the invalid code:

#include <iostream>

template<typename Type> void func(Type t)
{
    t.one();
}

template<typename Type> void func(Type t) // redefinition!
{
    t.two();
}

class One
{
    void one(void) const
    {
        std::cout << "one" << std::endl;
    }
};

class Two
{
    void two(void) const
    {
        std::cout << "two" << std::endl;
    }
};

int main(int argc, char* argv[])
{
    func(One()); // should print "one"
    func(Two()); // should print "two"
    return 0;
}

Is it possible to achieve using SFINAE? Is it possible to achieve using type_traits?


Clarification:

I would be more happy if this would be possible using SFINAE. The best-case scenario is: use first template, if it fails use the second one.

Checking for method existence is only an example. What I really want is also checking for compatibility with other classes.

The task could be rephrased:

  1. If the class supports the first interface, use it.
  2. If the first interface fails, use the second interface.
  3. If both fail, report an error.

Upvotes: 4

Views: 3619

Answers (3)

HeroicKatora
HeroicKatora

Reputation: 956

The code below

  • handles member function constness correctly
  • is agnostic of the functions return types
  • prints a comprehensive error on failure

It could be even shorter with C++14, where you don't have to specify the return types of implemented functions and have templated variable declarations. If you want to handle rvalue overloads correctly, you need to provide another overload to as_memfun.

If testing for member functions alone is not enough, there is another approach in the last section, which offers far better customization options but is also lengthier to setup.

#include <utility>
#include <functional>
namespace detail {
    template<typename T> struct _false : std::integral_constant<bool, false> { };
    template<typename T> struct HasNone {
        static_assert(_false<T>::value, "No valid method found");
    };

    template<typename T, typename R>
    constexpr auto as_memfun (R (T::* arg) ()) 
        -> R (T::*) () 
        { return arg; }
    template<typename T, typename R>
    constexpr auto as_memfun (R (T::* arg) () const)
        -> R (T::*) () const 
        { return arg; }
    template<typename T> constexpr auto check_has_two(int)
        -> decltype(as_memfun(&T::two)) 
        { return as_memfun(&T::two); }
    template<typename T> constexpr auto check_has_two(...)
        -> HasNone<T>;

    template<typename T> constexpr auto check_has_one(int)
        -> decltype(as_memfun(&T::one))
        { return as_memfun(&T::one); }
    template<typename T> constexpr auto check_has_one(...)
        -> decltype(check_has_two<T>(0))
        { return check_has_two<T>(0); }

    template<typename T>
    struct res { constexpr static auto detail = check_has_one<T>(0); };
}

template<typename T>
auto func(T t) -> decltype((t.*detail::res<T>::detail)()) {
    return (t.*detail::res<T>::detail)();
}

And here are some test you would probably like to have

struct One {
    void one();
};

struct Two {
    void two();
};

struct TestBoth {
    char one() const;
    void two();
};

struct TestWilderStuff {
    int one;
    void two() const;
};

int main() {
    func(One{});
    func(Two{});
    func(TestBoth{});
    static_assert(decltype(func(TestBoth{})){} == 0, "Failed function selection");
    func(TestWilderStuff{});
}

Since you seem to have more extensive constructions in mind than just testing for member function existence, here is the beginning of a vastly more powerful mechanism. You can use it as a drop-in replacement for the above solution and although it is far lengthier, it offers more customization and the possibility to do elaborate tests on your types in every step of the way.

#include <utility>
#include <functional>
namespace detail {
    template<typename T> struct _false :
        std::integral_constant<bool, false> { };
    template<typename T> struct HasNone {
        static_assert(_false<T>::value, "No valid method found");
    };

    // Generic meta templates used below
    namespace Generics {
        template<typename Getter, typename Else>
        struct ChainGetter {
            template<typename T> constexpr static auto get_(int)
                -> decltype(Getter::template get<T>())
                { return Getter::template get<T>(); }
            template<typename T> constexpr static auto get_(...)
                -> decltype(Else::template get<T>())
                { return Else::template get<T>(); }
            template<typename T> constexpr static auto get()
                -> decltype(get_<T>(0))
                { return get_<T>(0); }
        };

        template<typename Getter, typename Test>
        struct TestGetter {
            template<typename T, typename R> using _type = R;
            template<typename T> constexpr static auto get_()
                -> decltype(Getter::template get<T>())
                { return Getter::template get<T>(); }
            template<typename T> constexpr static auto test()
                -> decltype(Test::template test<T>(get_<T>()));
            template<typename T> constexpr static auto get()
                -> _type<decltype(test<T>()), 
                        decltype(get_<T>())
                        >
                { return get_<T>(); }
        };

        template<template<typename> class F>
        struct FailGetter {
            template<typename T>
            constexpr static auto get() -> F<T>;
        };
    }

    // Test only exists for member function pointer arguments
    struct IsMemberFunctionTest {
        template<typename _, typename T, typename R>
        constexpr static void test (R (T::* arg) ());
        template<typename _, typename T, typename R>
        constexpr static void test (R (T::* arg) () const);
    };

    // Get member pointer to T::one
    struct GetOne {
        template<typename T>
        constexpr static auto get() -> decltype(&T::one) { return &T::one; }
    };

    // Get member pointer to T::two
    struct GetTwo {
        template<typename T>
        constexpr static auto get() -> decltype(&T::two) { return &T::two; }
    };

    using namespace Generics;
    using getter_fail = FailGetter<HasNone>;
    using get_two_tested = TestGetter<GetTwo, IsMemberFunctionTest>;
    using getter_two = ChainGetter<get_two_tested, getter_fail>;
    using get_one_tested = TestGetter<GetOne, IsMemberFunctionTest>;
    using getter_one = ChainGetter<get_one_tested, getter_two>;

    template<typename T>
    struct result { constexpr static auto value = getter_one::template get<T>(); };
}

template<typename T>
auto func(T t) -> decltype((t.*detail::result<T>::value)()) {
    return (t.*detail::result<T>::value)();
}

Upvotes: 0

Richard Hodges
Richard Hodges

Reputation: 69882

Here's another way. There's a little more boilerplate, but in the actual expression of the different implementations of func() it could be argued that the 'list of tests that passed' is more expressive.

Food for thought anyway.

Code is c++11. c++14 and 17 would be more succinct.

#include <iostream>
#include <type_traits>
#include <tuple>

// boilerplate required prior to c++17
namespace notstd {
  using namespace std;
  template<typename... Ts> struct make_void { typedef void type;};
  template<typename... Ts> using void_t = typename make_void<Ts...>::type;
}

// test for having member function one()
template<class T, class Enable = notstd::void_t<>> struct has_one : std::false_type {}; 
template<class T> struct has_one<T, notstd::void_t<decltype(std::declval<T>().one())>> : std::true_type {};

//test for having member function two()
template<class T, class Enable = notstd::void_t<>> struct has_two : std::false_type {}; 
template<class T> struct has_two<T, notstd::void_t<decltype(std::declval<T>().two())>> : std::true_type {};

// a type collection of tests that pass
template<template <class...> class...Tests> struct passes_tests {
};

// meta-function to append a type
template<class Existing, template <class...> class Additional> struct append_pass;

template< template <class...> class...Tests, template <class...> class Additional>
struct append_pass<passes_tests<Tests...>, Additional> {
  using type = passes_tests<Tests..., Additional>;
};


//
// meta-functions to compute a list of types of test that pass 
//
namespace detail
{
  template<class Previous, class T, template<class...> class Test, template<class...> class...Rest>
  struct which_tests_pass_impl
  {
    using on_pass = typename append_pass<Previous, Test>::type;
    using on_fail = Previous;

    using this_term = typename std::conditional< Test<T>::value, on_pass, on_fail >::type;
    using type = typename which_tests_pass_impl<this_term, T, Rest...>::type;
  };

  template<class Previous, class T, template<class...> class Test>
  struct which_tests_pass_impl<Previous, T, Test>
  {
    using on_pass = typename append_pass<Previous, Test>::type;
    using on_fail = Previous;

    using this_term = typename std::conditional< Test<T>::value, on_pass, on_fail >::type;
    using type = this_term;
  };

}

template<class Type, template<class...> class...Tests> struct which_tests_pass
{
  using type = typename detail::which_tests_pass_impl<passes_tests<>, Type, Tests...>::type;
};


//
// various implementations of func()
//
namespace detail
{
  template<class T>
  void func(T t, passes_tests<has_one>)
  {
    t.one();
  }

  template<class T>
  void func(T t, passes_tests<has_one, has_two>)
  {
    t.one();
  }

  template<class T>
  void func(T t, passes_tests<has_two>)
  {
    t.two();
  }

  template<class T>
  void func(T t, passes_tests<>)
  {
    // do nothing
  }
}

template<class T>
void func(T t)
{
  detail::func(t, typename which_tests_pass<T, has_one, has_two>::type());
}

//
// some types
//
struct One
{
    void one(void) const
    {
        std::cout << "one" << std::endl;
    }
};

struct Two
{
    void two(void) const
    {
        std::cout << "two" << std::endl;
    }
};

// test
int main(int argc, char* argv[])
{
    func(One()); // should print "one"
    func(Two()); // should print "two"
    return 0;
}

Upvotes: 2

Yes, it's possible. In C++11 an onward it's even relatively easy.

#include <iostream>
#include <type_traits>

template<class, typename = void>
struct func_dispatch_tag :
  std::integral_constant<int, 0> {};

template<class C>
struct func_dispatch_tag<C, 
  std::enable_if_t<std::is_same<decltype(&C::one), void (C::*)() const>::value>
  > : std::integral_constant<int, 1> {};

template<class C>
struct func_dispatch_tag<C,
  std::enable_if_t<std::is_same<decltype(&C::two), void (C::*)() const>::value>
  > : std::integral_constant<int, 2> {};

template<class C>
void func(C const&, std::integral_constant<int, 0>) {
    std::cout << "fallback!\n";
}

template<class C>
void func(C const &c, std::integral_constant<int, 1>) {
    c.one();
}

template<class C>
void func(C const &c, std::integral_constant<int, 2>) {
    c.two();
}

template<class C>
void func(C const &c) {
    func(c, func_dispatch_tag<C>{});
}

struct One
{
    void one(void) const
    {
        std::cout << "one\n";
    }
};

struct Two
{
    void two(void) const
    {
        std::cout << "two\n";
    }
};

struct Three {};

int main(int argc, char* argv[])
{
    func(One()); // should print "one"
    func(Two()); // should print "two"
    func(Three());
    return 0;
}

Important points:

  1. We SFINAE on the second parameter of func_dispatch_tag. The compiler looks at all the template specializations which result in the parameters <C, void>. Since any of the latter is "more specialized" when SF doesn't occur (i.e when std::enable_if_t is void), it gets chosen.

  2. The chosen specialization of the trait defines a tag which we do a tag dispatch on. Tag dispatch depends on function overloading, instead of function template specialization (that cannot be partially specialized).

  3. You can define a fallback function (like I did), or static_assert. The number of tags we can define is limited only by the range of an int, so extending to other members is just a matter of adding another func_dispatch_tag specialization.

  4. The member must be accessible, or SF will occur. Also, a class that has both members will result in ambiguity. Bear that in mind.

Upvotes: 6

Related Questions