1

(i don't think it's a duplicate of Template member function specialization of a templated class without specifying the class template parameter reading it was a bit difficult, so i might be wrong. i also found no other possibly relevant questions)

is there a way to overload functions based on the instances template type?

in my case, i have a number class that implements a modulo operator

template <typename X>
class Operand
{
private:
        X _val;
public:
        Operand *operator% (const Operand &other_number ) const
    {
       //here, i would like use fmod for floats and doubles, and % for integers
    }
];

of course, there's always the solution of casting everything into long doubles, using fmod, and casting back into X, but it feels

  • terribly inefficient
  • Very awkward
  • not very idiomatic to C++
  • and probably pretty problem prone somehow

4 Answers4

2

Since C++17, using if constexpr is very practical. Though, if you have many differences you may want to separate the floating point implementation from the integral type implementation completely.

All the below implementations require #include <type_traits> and should also be followed by this free function:

template<class X>
Operand<X> operator%(const Operand<X>& l, const Operand<X>& r) {
    Operand rv{l};
    rv %= r;
    return rv;
}

One way of doing it would be to use the CRTP:

template<class, bool> struct impl;

template<template<class> class O, class X>
struct impl<O<X>, true> { // specialization for floating point types
    using op_type = O<X>;
    // implement all special floating point behavior here:
    op_type& operator%=(const op_type& other) {
        op_type* This = static_cast<op_type*>(this);
        This->val = std::fmod(This->val, other.val);
        return *This;
    }
};

template<template<class> class O, class X>
struct impl<O<X>, false> { // specialization for integral types
    using op_type = O<X>;
    // implement all special functions for integral types here:
    op_type& operator%=(const op_type& other) {        
        auto This = static_cast<op_type*>(this);
        This->val %= other.val;
        return *This;
    }
};

template <class X, std::enable_if_t<std::is_arithmetic_v<X>, int> = 0>
class Operand : public impl<Operand<X>, std::is_floating_point_v<X>> {
private:
    using impl_type = impl<Operand<X>, std::is_floating_point_v<X>>;
    friend  impl_type;
    X val;
public:
    Operand() = default;
    Operand(X v) : val(v) {}
    // implement functions common to integral and floating point types here
};

Without CRTP, it could look like this:

template<class, bool> struct impl;

template<class X>
struct impl<X, true> { // specialization for floating point types
    X remainder(X l, X r) const {
        return std::fmod(l, r);
    }
};

template<class X>
struct impl<X, false> { // specialization for integral types
    X remainder(X l, X r) const {
        return l % r;
    }
};

template <class X, std::enable_if_t<std::is_arithmetic_v<X>, int> = 0>
class Operand : private impl<X, std::is_floating_point_v<X>> {
private:
    X val;
public:
    Operand& operator%=(const Operand& other) {
        val = remainder(val, other.val);
        return *this;
    }
};

If you want to select the member function implementation using SFINAE:

template <class X, std::enable_if_t<std::is_arithmetic_v<X>, int> = 0>
class Operand {
private:
    X val{};
public:
    Operand() = default;
    Operand(X v) : val(v) {}

    template<class Y = X, std::enable_if_t<std::is_same_v<X, Y>, int> = 0>
    std::enable_if_t<std::is_floating_point_v<Y>, Operand&>
    operator%=(const Operand& other) {
        val = std::fmod(val, other.val);
        return *this;
    }
    template<class Y = X, std::enable_if_t<std::is_same_v<X, Y>, int> = 0>
    std::enable_if_t<std::is_integral_v<Y>, Operand&>
    operator%=(const Operand& other) {
        val %= other.val;
        return *this;
    }
};

Since C++17, using constexpr if is often preferred over the SFINAE method:

template <class X, std::enable_if_t<std::is_arithmetic_v<X>, int> = 0>
class Operand {
private:
    X val{};
public:
    Operand() = default;
    Operand(X v) : val(v) {}

    Operand& operator%=(const Operand& other) {
        if constexpr (std::is_floating_point_v<X>) {
            val = std::fmod(val, other.val);
        } else {
            val %= other.val;
        }        
        return *this;
    }
};
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • Wow, that's the kind of awnser that tells me i still have to learn many things i have a few questions, for clarifications how does the keyword "using" work within the class? is struct impl simply meant to hold two types? if so, how can bool become true? (true is a value, not a type, right?) what does template accomplish, and how is it different from template (classes are types, right?) while this is pretty clear on what it's supposed to do, the syntax makes the details pretty difficult to grasp thanks for awnsering! – GDGiantDwarf Jul 08 '22 at 11:34
  • @GDGiantDwarf `using` is like `typedef`, so one could have written `typedef impl, std::is_floating_point_v> impl_type;` instead. The `template` (without implementation) is just a placeholder to have something to specialize. Yes, `class` and `typename` have the same meaning in this context (although I think there _has_ been some difference in the past, I don't remember exactly). Yes, it looks a bit cumbersome but if one has many methods that differ between the implementations it can be nice to see them all floating point implementations together etc. – Ted Lyngmo Jul 08 '22 at 11:43
  • @GDGiantDwarf I added a range of other options to the answer. – Ted Lyngmo Jul 08 '22 at 14:17
0

Are you looking for something like this?

if constexpr (std::is_same_v<int, X>) {
    // use %
} else if constexpr (std::is_same_v<double, X> || std::is_same_v<float, X>) {
    // use fmod
} else {
    static_assert(!std::is_same_v<int, X>
               && !std::is_same_v<double, X>
               && !std::is_same_v<float, X>,
            "Unsupported type");
}

Ugly solution, but maybe it's what you're looking for. I mean, if the number of cases is not gonna change, then it's probably ok.

However, I would question the overall design and look for topics like CPO.


If you look for an "overload-based" solution, than you can specialize the operator% for the various types you need:

template <typename X>
class Operand
{
private:
        X _val;
public:
    Operand *operator% (const Operand &other_number ) const;
};

template<>
Operand<int> *Operand<int>::operator%(const Operand<int> &) const {
    // impl
}
template<>
Operand<double> *Operand<double>::operator%(const Operand<double> &) const {
    // impl
}
template<>
Operand<float> *Operand<float>::operator%(const Operand<float> &) const {
    // impl
}

Needless to say, this is a ugly solution as well.

Enlico
  • 23,259
  • 6
  • 48
  • 102
  • 3
    You also have the [`std::is_floating_point`](https://en.cppreference.com/w/cpp/types/is_floating_point) and [`std::is_integral`](https://en.cppreference.com/w/cpp/types/is_integral) type traits so that you don't have to check every possible integral type etc. – Ted Lyngmo Jul 08 '22 at 10:41
  • That does seem pretty clear! However, it seems very odd that there would be no way to have an overloading based solution, is there really none? in the meanwhile, i'll use this, thanks for awnsering <3 – GDGiantDwarf Jul 08 '22 at 10:41
  • @GDGiantDwarf, I've added the "overload-based" solution which you were probably thinking of? – Enlico Jul 08 '22 at 15:47
  • @Enlico i wasn't "thinking of" it as much as i was presuming it existed and didn't really know the syntax (or how to look for that specifically, if we're being honest) here, though, a type is expected for the other value as well, which create some problems however, your first solution, as well as Ted's were very nice to solve my problem (as well as pointing me to a lot more of c++ i hadn't even started approaching yet) your awnsers are very helpful, thank you very much – GDGiantDwarf Jul 08 '22 at 17:05
  • @GDGiantDwarf, I've added another answer that might be interesting. – Enlico Jul 08 '22 at 23:06
0

I think the cleanest way to overload a function based on a template type is using the C++20 constraints. You can do something like the following:

template <typename X>
class Operand
{
    private:
        X val;
    public:
        Operand(X val): val{val} {}

        template<typename Y>
        requires std::same_as<Y, int>
        friend Operand operator% (const Operand<Y> lhs, const Operand<Y> &rhs ) {
            //here, Y is int
            return Operand<Y> { lhs.val % rhs.val };
        }

        template<typename Y>
        requires std::same_as<Y, float>
        friend Operand operator% (const Operand<Y> lhs, const Operand<Y> &rhs ) {
            //here, Y is float
            return Operand<Y> { std::fmod(lhs.val, rhs.val) };
        }
};

Note that this also uses non-member friend definitions, which is more symmetric and easier to read in my opinion.

If you don't have a C++20 compiler, you can use std::enable_if for exactly the same effect (but with uglier compiler error messages if no matching operator is found). std::enable_if can be used in various ways, from which I prefer the following one:

template<typename Y, typename std::enable_if<std::is_same<Y, int>::value, bool>::type = true>
friend Operand operator% (const Operand<Y> lhs, const Operand<Y> &rhs ) {
    //here, Y is int
    //...
}

If you find yourself doing this for many more operators/functions, you should propably completely separate the implementations for the different types, like e.g. shown in the answer of Ted Lyngmo.

Jakob Stark
  • 3,346
  • 6
  • 22
0

When I mentioned CPOs in the other answer, I was referring to techniques taht are discussed a bit here.

However, even without going to those lengths, we can take just some inspiration:

  • implement Operand<T>::operator% in terms of a helper function acting on the _vals which are extracted from operator%'s arguments and passed to helper,
  • overload/template-and-specialize helper for the types (of _val) you want to enable;
  • in operator%, the call to helper should be unqualified, which has the advantage that you can rely on ADL for the call to be successful as long as the matching helper overload and at least one argument's type are in defined in the same namespace (well, not necessarily the same; study ADL for the details);
  • to take into account that sometimes you can't define helper in the same namespace as the argument you want to use it on (e.g., if you were planning to use Operand<std::vector<int>>, you can't define helper in namespace std { /* here */ } because that's illegal), let helper take a first additonal argument of type Operand, which acts as a tag and allows you to define helper in Operand's namespace.

Here's a rough attempt:

#include <math.h>
#include <iostream>

namespace operations { // namespace where Operand lives
template <typename X>
class Operand {
private:
public:
    struct Tag {};
    X _val;
    Operand() {};
    Operand(X v) : _val{v} {};
    Operand* operator% (Operand const& other_number ) const {
        std::cout << "dispatches to: ";
        return new Operand{helper(*this, this->_val, other_number._val)};
    }
};
}

using operations::Operand;

namespace operations {
// for builtin types, we need to resort to the namespace
// where Operand (the tag) is defined

// non-templated overload for int specifically
int helper(Operand<int> const&, int a, int b) {
    std::cout << "int" << std::endl;
    return a % b;
}

// same holds for those templates which we expect
// to instatiate for builtin types or types in std::

// templated overload for non-integrals
template<typename T, std::enable_if_t<!std::is_integral_v<T>, bool> = true>
auto helper(Operand<T> const&, T a, T b) {
    std::cout << "non-integrals" << std::endl;
    return std::fmod(a, b);
}

// templated overload for integrals
template<typename T, std::enable_if_t<std::is_integral_v<T>, bool> = true>
auto helper(Operand<T> const&, T a, T b) {
    std::cout << "other integrals" << std::endl;
    return a % b;
}
}

namespace myspace {
struct MyType {
    MyType(int) {}
};

// can be implemented in the namespace where MyType lives

// overload for MyType
MyType helper(Operand<MyType> const&, MyType const&, MyType const&) {
    std::cout << "MyType" << std::endl;
    return 0;
}
}

int main() {
    Operand<int> i1{15};
    Operand<int> i2{6};
    Operand<unsigned int> u1{15};
    Operand<unsigned int> u2{6};
    Operand<double> d1{15};
    Operand<double> d2{6};
    Operand<float> f1{15};
    Operand<float> f2{6};
    Operand<myspace::MyType> m1{0};
    Operand<myspace::MyType> m2{0};

    (i1 % i2); // dispatches to: int
    (u1 % u2); // dispatches to: other integrals
    (d1 % d2); // dispatches to: non-integrals
    (f1 % f2); // dispatches to: non-integrals
    (m1 % m2); // dispatches to: MyType
}

But I really suggest to go through those readings I linked and those linked from there. CPOs are most important in generic programming.

Enlico
  • 23,259
  • 6
  • 48
  • 102