声明 const 时模板成员函数解析失败

template member function resolution fails when declaring const

下面的代码显示了一些有趣的行为:

#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);
}

我的预期输出是

foo_B
foo_F

但实际输出是

foo_F
foo_F

这取决于void foo(B<T> &x)是否声明const。如果省略 const,则输出符合预期。

此外,如果将 const 添加到 void foo(F f),输出也符合预期。

但是,void foo(B<T> &x) 不会更改 this,而 void foo(F f) 会更改 this。所以当前的布局是必需的。

非常感谢知道如何在不删除 const 的情况下解决此问题。

资格转换(此处:在隐式对象参数上)不是身份转换,并且在重载解析排名中有成本

成员函数void foo(B<T>& x) constconst限定的,而模板成员函数template<class F> void foo(F f)不是。这意味着后者更适合隐式对象参数为 not const 的调用,根据 [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.

[over.match.best]/2.1

如果在main中const限定自动变量b,将选择非模板重载:

// ^^^ `foo` overloads as in OP's example
B<int> a{};
B<int> const b{}
b.foo(a);  // foo_B

如果您改为 const-quality 模板成员函数 foo,则非模板将与模板重载相同匹配(隐式对象参数需要 const 限定),在这种情况下,根据 [over.match.best]/2.4.

选择非模板函数作为最佳可行重载

如果您不想让特定重载参与 类型谓词 的重载决议:删除它

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)

当其类型模板参数的模板实参是 B class 模板的特化时,您可以使用特征删除模板成员函数:

#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);
}

我们利用了 P2098R1is_specialization_of 特性(请注意,这对于作为别名模板的模板参数有实现差异 - IIRC 有点不明确)。

请注意,使用这种方法,重载的 none 对于 另一个 特化 B 的参数(而不是隐式对象参数).

这里的问题是,由于 void foo(B<T> &x)const; 是 const 限定的,它必须对您调用函数的对象进行 const 限定。这与 template<class F> void foo(F f); 提供的匹配不完全相同,因为它不需要进行 const 限定。这就是它用于两个调用的原因。

您也可以通过 const 限定模板版本来解决此问题,例如:

#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);
}

哪个会打印

foo_B
foo_F

另一种选择是使用 SFINAE 来限制模板版本排除 B<T>。那看起来像

#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);
}

并且与第一个示例具有相同的输出。

一个相当简单但有点冗长的方法是在你想调用 const 版本时将对象转换为 const:

static_cast<const B<int>>(b).foo(a);

这足以让调用显示 foo_B...