【问题标题】:template member function resolution fails when declaring const声明 const 时模板成员函数解析失败
【发布时间】:2021-11-14 04:18:05
【问题描述】:

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

#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&lt;T&gt; &amp;x) 是否被声明为const。如果const 被省略,则输出符合预期。

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

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

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

【问题讨论】:

    标签: c++ function templates lambda member


    【解决方案1】:

    资格转换(这里:在隐式对象参数上)不是身份转换,并且在重载决议排名中具有成本

    成员函数void foo(B&lt;T&gt;&amp; x) constconst-qualified,而模板成员函数template&lt;class F&gt; void foo(F f) 不是。这意味着后者更适合隐式对象参数为 not const 的调用,根据 [over.ics.rank]/3.2.5:

    标准转换序列 S1 是比标准转换序列更好的转换序列 标准转换序列S2 if

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

    [over.match.best]/2.1

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

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

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


    如果您不希望特定的重载参与类型谓词的重载解析:删除它

    但是,b 在实际应用中不能声明为 const,归结为复制,比如 const c(b);然后使用 c.foo(a)

    当模板参数的类型模板参数的模板参数是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 特征(请注意,这对于作为别名模板的模板参数有实现差异 - 有点未指定的 IIRC)。

    请注意,使用这种方法,对于 B另一个 特化的参数(而不是隐式对象参数的参数),任何重载都不可行。

    【讨论】:

    • 嗨。当然是一种方法。但是,b 在实际应用中不能声明为const,归结为复制一份,比如const c(b);,然后使用c.foo(a)。由于这两个函数的主体可能有很大不同,因此忘记后者(即使用 b.foo(a) 代替)是一个段错误保证。更好的解决方案是编译器会引发标志的结构。
    • @user1407220 当模板参数是B 的特化时,我使用 SFINAE 方法进行扩展以删除模板重载。
    【解决方案2】:

    这里的问题是,由于 void foo(B&lt;T&gt; &amp;x)const; 是 const 限定的,它必须 const 限定你正在调用函数的对象。这并不像template&lt;class F&gt; 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&lt;T&gt; 的版本。看起来像

    #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 只是为了突出我的方法之间的区别,因为它们很微妙:这将为B 的不同专业化的参数调用foo_F 重载,这可能是也可能不是OP想要什么。例如。将上面最后一个示例中 a 的类型更改为例如B&lt;double&gt; a; 将为b.foo(a); 调用foo_F 重载。
    • @dfrib 虽然是这样,但如果你做了同样的事情,你的代码将不会编译,因为void foo(B&lt;T&gt; &amp;x) 不是函数模板。这就是我这样做的原因。目前尚不清楚 OP 想要的行为到底是什么。
    • @NathanOliver 是的。
    【解决方案3】:

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-07-31
      • 2021-03-07
      • 2016-05-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多