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

3
以下代码展示了一些有趣的行为:
#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,则输出结果符合预期。

此外,如果向void foo(F f)添加const,那么输出结果也符合预期。

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

如何在不放弃const的情况下解决这个问题,任何想法都会受到欢迎。

3个回答

2

一种资格转换(这里指隐式对象参数)不是标识转换,并且在重载解析排名中具有成本

成员函数void foo(B<T>& x) constconst限定,而模板成员函数template<class F> void foo(F f)则没有。这意味着后者更适合于隐式对象参数不是常量的调用,根据[over.ics.rank]/3.2.5

如果标准转换序列S1比标准转换序列S2更好,则S1是更好的转换序列

  • [...], 或者,如果不是这样,S1和S2仅在其资格转换([conv.qual])上有所不同,并分别产生相似的类型T1和T2,其中T1可以通过资格转换转换为T2。

[over.match.best]/2.1

如果在主函数中对自动变量 b 进行 const 限定,非模板重载将被选择:

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

如果您将模板成员函数 foo 设为 const 限定,则非模板函数将与模板重载匹配(需要对隐式对象参数进行 const 限定),此时,根据 [over.match.best]/2.4 的规定,非模板函数将被选为最佳可行重载。

如果您不希望特定的重载参与类型谓词的重载决议:请将其删除

然而,在实际应用中,b 无法声明为 const,这归结为创建一个副本,比如 const c(b); 然后使用 c.foo(a)

您可以使用一个 trait,在其类型模板参数的特化是 B 类模板时,移除模板成员函数:

#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特性(请注意,对于别名模板的模板参数,这具有实现差异 - 在我记忆中有点欠规范)。
请注意,使用这种方法,对于参数是“另一个”B的特化(而不是隐式对象参数的那个特化),所有重载都不可行。

嗨。这确实是一种方法。但是,在实际应用中无法将b声明为const,这意味着需要制作一个副本,例如const c(b);,然后使用c.foo(a)。由于两个函数的主体可能有很大不同,忘记后者(即使用b.foo(a))会导致段错误。更好的解决方案是使用结构体,让编译器发出警告。 - user1407220
@user1407220,我使用SFINAE方法进行了扩展,以消除当模板参数为B的特化时的模板重载。 - dfrib

2
这里的问题是由于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);
}

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

嗨,谢谢回复。但是这种方法无法解决问题,因为foo(F f)必须能够更改对象,如果声明为const则无法实现。 - user1407220
@user1407220 我已经更新了答案,提供了一个可以实现该功能的版本。 - NathanOliver
@user1407220 只是为了强调我的方法与其他方法的微妙差别:对于不同特化的 B 的参数,这将调用 foo_F 重载,这可能是 OP 想要的,也可能不是。例如,将上面最后一个示例中 a 的类型更改为 B<double> a; 将调用 b.foo(a);foo_F 重载。 - dfrib
@dfrib 如果你这样做,你的代码将无法编译,因为 void foo(B<T> &x) 不是一个函数模板。这就是为什么我要这样做的原因。不清楚 OP 想要的确切行为。 - NathanOliver
@NathanOliver 是的,没错。 - dfrib
无论如何,void foo(B<T> &x) 应该明确表明 B 的类型将始终与 this 的类型相同。 - user1407220

0
一个相对简单但有点啰嗦的方法是在调用const版本时将对象转换为const:
static_cast<const B<int>>(b).foo(a);

这已足够用于调用显示foo_B...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接