Home > Enterprise >  template member function resolution fails when declaring const
template member function resolution fails when declaring const

Time:09-21

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.

and [over.match.best]/2.1

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...

  • Related