3

The original working code

I have a class template with two template parameters and an optimized operator== when the two types are the same and one another condition is satisfied. My original code is as follows (for demonstration purposes I make the generic comparison return false and the one where T1 == T2 return true):

template<typename T1, typename T2>
struct my_class
{
    // A few hundred LOCs
};

template<typename U1, typename U2>
bool operator==(my_class<U1, U2> const& lhs, my_class<U1, U2> const& rhs)
{
    return false;
}

template<typename U>
auto operator==(my_class<U, U> const& lhs, my_class<U, U> const& rhs)
    -> std::enable_if_t<some_condition, bool>
{
    return true;
}

The idea is that the first overload of operator== is the default, and when both U1 and U2 are the same type and some_condition is satisfied the second overload is valid and picked as a better match.

The problem

I recently started implementing more and more operators in my generic libraries as hidden friends to avoid some unwanted implicit conversions and to reduce the overload set the compiler has to choose from at namespace scope.

I first tried the most obvious approach to friends, which is to move the definitions as is in the class template and to prefix them with friend:

template<typename T1, typename T2>
struct my_class
{
    // A few hundred LOCs

    template<typename U1, typename U2>
    friend bool operator==(my_class<U1, U2> const& lhs, my_class<U1, U2> const& rhs)
    {
        return false;
    }

    template<typename U>
    friend auto operator==(my_class<U, U> const& lhs, my_class<U, U> const& rhs)
        -> std::enable_if_t<some_condition, bool>
    {
        return true;
    }
};

Much not to my surprise this didn't work and I got redefinition errors. The reason why is explained - along with a standard quote - in this answer.

An attempt

We are always comparing my_class with matching template parameters, so I figured that I could get rid of the inner template<typename U1, typename U2> in the first definition, however it is trickier in the second one since the single template parameter was used to create a more specialized overload of operator==. Another solution would be to put that overload in a specialization of my_class<T, T> but since the class is big I didn't feel like duplicating its contents since pretty much everything else is the same. I could probably introduce another layer of indirection for the common code but I already have a hefty amount of indirection.

Failing that I tried to fallback to good old SFINAE to make sure that T1 and T2 are the same:


template<typename T1, typename T2>
struct my_class
{
    // A few hundred LOCs

    friend auto operator==(my_class const& lhs, my_class const& rhs)
        -> bool
    {
        return false;
    }

    friend auto operator==(my_class const& lhs, my_class const& rhs)
        -> std::enable_if_t<std::is_same<T1, T2>::value && some_condition, bool>
    {
        return true;
    }
};

For some reason I don't claim to fully understand the second operator== above is actually ill-formed, but we can get around that by adding back some additional defaulted template parameters into the mix:

template<typename T1, typename T2>
struct my_class
{
    // A few hundred LOCs

    friend auto operator==(my_class const& lhs, my_class const& rhs)
        -> bool
    {
        return false;
    }

    template<typename U1=T1, typename U2=T2>
    friend auto operator==(my_class const& lhs, my_class const& rhs)
        -> std::enable_if_t<std::is_same<U1, U2>::value && some_condition, bool>
    {
        return true;
    }
};

This compiles as expected, but comparing two instances of my_class with matching T1 and T2 now returns false because the second overload of operator== is less specialized than the first one. An educated guess tells me that the new template layer is the reason, so I added back template parameters to the first overload of operator== and also an SFINAE condition which is a negation of the other one to make sure that the overload wouldn't be ambiguous with matching T1 and T2:

template<typename T1, typename T2>
struct my_class
{
    // A few hundred LOCs

    template<typename U1=T1, typename U2=T2>
    friend auto operator==(my_class const& lhs, my_class const& rhs)
        -> std::enable_if_t<not(std::is_same<U1, U2>::value && some_condition), bool>
    {
        return false;
    }

    template<typename U1=T1, typename U2=T2>
    friend auto operator==(my_class const& lhs, my_class const& rhs)
        -> std::enable_if_t<std::is_same<U1, U2>::value && some_condition, bool>
    {
        return true;
    }
};

This finally gives the intended result while also providing the benefits of hidden friends, but the cost is a bit high in term of readability and maintainability.

Back to the question I originally meant to ask

I tried to explain my problem and rough solution and how I got to it above. My question is: is there a better way to achieve the same result (hidden friends with my code) without having to dive into all the template issues I highlighted above? Can I have such hidden friends while relying on the built-in partial ordering of function templates instead of replacing it with another layer of SFINAE as I did?

PiotrNycz
  • 23,099
  • 7
  • 66
  • 112
Morwenn
  • 21,684
  • 12
  • 93
  • 152
  • It may make sense to get rid of the implicit conversions instead of trying to make the nuisance that these cause bit smaller. – Öö Tiib Feb 19 '20 at 21:42
  • @ÖöTiib It's not so much about the implicit conversions than about learning more about this specific corner of the language and about considering what I might have missed tbh – Morwenn Feb 19 '20 at 21:44
  • 1
    I would perhaps use "if constexpr()" in single operator== instead of two overloads. – Öö Tiib Feb 19 '20 at 22:01

2 Answers2

3

Would you accept this?

template<typename T1, typename T2>
struct my_class
{
    friend bool operator==(my_class const& lhs, my_class const& rhs)
    {
        if constexpr (std::is_same_v<T1, T2>) {
            return condition;
        } else {
            return false;
        }
    }
};
Barry
  • 286,269
  • 29
  • 621
  • 977
  • I'm so used to having a return type of the form `decltype(returned expression)` where expression SFINAE matters that I kind of forgot about `if constexpr`. Here it surprisingly enough does solve the specific problem I have, even if might not work for the more general case. Thanks! – Morwenn Feb 19 '20 at 22:23
1

The Barton&Nackman_trick will work in your case:

See:

template<typename T1, typename T2>
struct my_class;
struct my_class_friend_operators
{
    template<typename U1, typename U2>
    friend bool operator==(my_class<U1, U2> const& lhs, my_class<U1, U2> const& rhs)
    {
        return false;
    }

    template<typename U>
    friend bool operator==(my_class<U, U> const& lhs, my_class<U, U> const& rhs)
    {
        return true;
    }
};

template<typename T1, typename T2>
struct my_class : my_class_friend_operators 
{
    // A few hundred LOCs
};

int main() {
    return my_class<float,int>{} == my_class<float,int>{}
        && my_class<int,int>{} == my_class<int,int>{};
}

Working demo

The extra benefit is that you can have all of your // A few hundred LOCs not mixed with your friend operators.

As explained on this wiki link - friend of base classes are also considered during ADL - so in this way you can happily have "Hidden Friend Idiom" in your template.

PiotrNycz
  • 23,099
  • 7
  • 66
  • 112
  • I know the question is pretty old -- but I just have similar problem and I've found the solution so why not share with others... – PiotrNycz Mar 01 '23 at 14:49