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?