Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.
like image 520
haael Avatar asked Feb 05 '23 09:02

haael


1 Answers

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.

like image 96
StoryTeller - Unslander Monica Avatar answered Feb 12 '23 07:02

StoryTeller - Unslander Monica