the code below shows some interesting behavior:
#include <iostream>
using namespace std;
template<class T>
class B{
public:
void foo(B<T> &x)const;
template<class F> void foo(F f);
};
template<typename T> void B<T>::foo(B<T> &x)const{cout<<"foo_B"<<endl;}
template<typename T> template<typename F> void B<T>::foo(F f){cout<<"foo_F"<<endl;}
int main(){
B<int> a;
B<int> b;
b.foo(a);
b.foo([](){;});
return(0);
}
my expected output is
foo_B
foo_F
but the actual output is
foo_F
foo_F
it depends on whether void foo(B<T> &x)
is declared const
. If const
is omitted the output is as expected.
Further, if const
is added to void foo(F f)
the output is as expected as well.
However, void foo(B<T> &x)
will not change the this
, whereas void foo(F f)
will change this
. So the current layout is the one required.
Any idea how to resolve this without dropping const
is much appreciated.
CodePudding user response:
The issue here is that since void foo(B<T> &x)const;
is const qualified, It would have to const qualify the object you are calling the function on. This isn't as exactly as a match as template<class F> void foo(F f);
provides as it doesn't need to do that const qualification. That is why it is used for both calls.
You can fix this by also const qualifying the template version like:
#include <iostream>
using namespace std;
template<class T>
class B{
public:
void foo(B<T> &x)const;
template<class F> void foo(F f)const;
};
template<typename T> void B<T>::foo(B<T> &x)const{cout<<"foo_B"<<endl;}
template<typename T> template<typename F> void B<T>::foo(F f)const{cout<<"foo_F"<<endl;}
int main(){
B<int> a;
B<int> b;
b.foo(a);
b.foo([](){;});
return(0);
}
Which will print
foo_B
foo_F
Another option would be to use SFINAE to constrain the template version from excepting B<T>
's. That would look like
#include <iostream>
using namespace std;
template<class T>
class B{
public:
void foo(B<T> &x)const;
template<class F, std::enable_if_t<!std::is_same_v<B<T>, F>, bool> = true>
void foo(F f);
};
template<typename T> void B<T>::foo(B<T> &x)const{cout<<"foo_B"<<endl;}
template<typename T> template<class F, std::enable_if_t<!std::is_same_v<B<T>, F>, bool>>
void B<T>::foo(F f){cout<<"foo_F"<<endl;}
int main(){
B<int> a;
B<int> b;
b.foo(a);
b.foo([](){;});
return(0);
}
and has the same output as the first example.
CodePudding user response:
A qualification-conversion (here: on the implicit object parameter) is not an identity conversion, and has a cost in overload resolution ranking
The member function void foo(B<T>& x) const
is const
-qualified, whereas the template member function template<class F> void foo(F f)
is not. This means the latter is a better match for a call where the implicit object parameter is not const, as per [over.ics.rank]/3.2.5:
Standard conversion sequence S1 is a better conversion sequence than standard conversion sequence S2 if
- [...], or, if not that, S1 and S2 differ only in their qualification conversion ([conv.qual]) and yield similar types T1 and T2, respectively, where T1 can be converted to T2 by a qualification conversion.
If you const
-qualify the automatic variable b
in main, the non-template overload will be chosen:
// ^^^ `foo` overloads as in OP's example
B<int> a{};
B<int> const b{}
b.foo(a); // foo_B
If you instead const
-quality the template member function foo
, then the non-template will be the same match as the template overload (const-qualification needed on implicit object parameter), in which case the non-template function is chosen as the best viable overload, as per [over.match.best]/2.4.
If you never want a particular overload to participate in overload resolution for type predicate: remove it
However, b cannot be declared const in the actual application, which boil down to making a copy, say const c(b); and then use c.foo(a)
You could use a trait to remove the template member function when the template argument for its type template parameter is a specialization of the B
class template:
#include <iostream>
#include <type_traits>
template <class T, template <class...> class Primary>
struct is_specialization_of : std::false_type {};
template <template <class...> class Primary, class... Args>
struct is_specialization_of<Primary<Args...>, Primary> : std::true_type {};
template <class T, template <class...> class Primary>
inline constexpr bool is_specialization_of_v{is_specialization_of<T, Primary>::value};
template <class T> class B {
public:
void foo(B<T> &x) const { std::cout << "foo_B" << std::endl; }
template <class F, typename = std::enable_if_t<!is_specialization_of_v<F, B>>>
void foo(F f) {
std::cout << "foo_F" << std::endl;
}
};
int main() {
B<int> a;
B<int> b;
b.foo(a);
b.foo([]() { ; });
return (0);
}
where we've leveraged the is_specialization_of
trait of P2098R1 (just note that this has implementation variances for template arguments that are alias templates - somewhat underspecified IIRC).
Note that with this approach none of the overloads would be viable for an argument that is another specialization of B
(than that of the implicit object parameter).
CodePudding user response:
A rather simple even if a bit verbose way is to cast the object to const when you want to call the const version:
static_cast<const B<int>>(b).foo(a);
This is enough for the call to display foo_B
...