为什么不能在std::reference_wrapper ' s中推导出模板实例?

Why can template instances not be deduced in `std::reference_wrapper`s?

本文关键字:实例 std 不能 reference 为什么 wrapper      更新时间:2023-10-16

假设我有一个类型为T的对象,我想把它放在一个引用包装器中:

int a = 5, b = 7;
std::reference_wrapper<int> p(a), q(b);   // or "auto p = std::ref(a)"

现在我可以很容易地说if (p < q),因为引用包装器对其包装类型进行了转换。一切都很顺利,我可以处理引用包装器的集合,就像它们是原始对象一样。

(正如下面链接的问题所示,这是生成现有集合的备用视图的一种有用方法,可以随意重新排列,而不会产生完整副本的成本,并保持与原始集合的更新完整性。)


然而,对于某些类,这不起作用:

std::string s1 = "hello", s2 = "world";
std::reference_wrapper<std::string> t1(s1), t2(s2);
return t1 < t2;  // ERROR

我的解决方法是定义一个谓词,如在这个答案*;但我的问题是:

为什么以及何时可以将操作符应用于引用包装器并透明地使用包装类型的操作符?为什么std::string会失败?它与std::string是模板实例有什么关系?

*)更新:根据答案,似乎使用std::less<T>()是一个通用的解决方案。

编辑:将我的猜测移到底部,这里是规范性文本为什么这不会工作。TL;博士版:

如果函数形参包含推导出的模板形参,则不允许转换。


§14.8.3 [temp.over] p1

[…当使用操作符(显式或隐式)编写对该名称的调用时对每个函数模板执行表示法)、模板实参推导(14.8.2)和任何显式模板实参检查(14.3),以查找可与该函数模板一起使用的模板实参值(如果有的话),以实例化可通过调用实参调用的函数模板特化。

§14.8.2.1 [temp.deduct.call] p4

[…[注意:如14.8.1中所述,如果函数实参中没有模板形参参与模板实参演绎,则隐式转换将对函数实参执行转换,将其转换为相应的函数形参的类型。[…> >

§14.8.1 [temp.arg.explicit] p6

如果函数实参类型不包含参与模板实参演绎的模板形参,则将对函数实参执行隐式转换(第4章),将其转换为相应的函数形参类型。[注意:如果显式指定了模板形参,则不参与模板实参推导。[…> >

由于std::basic_string依赖于推导的模板形参(CharT, Traits),因此不允许进行转换。


这是鸡生蛋还是蛋生鸡的问题。为了推导出模板参数,它需要一个std::basic_string的实际实例。要转换为包装类型,需要一个转换目标。这个目标必须是一个实际的类型,而类模板不是。编译器必须根据转换操作符或类似的东西测试std::basic_string的所有可能的实例化,这是不可能的。

假设以下最小测试用例:

#include <functional>
template<class T>
struct foo{
    int value;
};
template<class T>
bool operator<(foo<T> const& lhs, foo<T> const& rhs){
    return lhs.value < rhs.value;
}
// comment this out to get a deduction failure
bool operator<(foo<int> const& lhs, foo<int> const& rhs){
    return lhs.value < rhs.value;
}
int main(){
    foo<int> f1 = { 1 }, f2 = { 2 };
    auto ref1 = std::ref(f1), ref2 = std::ref(f2);
    ref1 < ref2;
}

如果没有为int上的实例化提供重载,则推导失败。如果我们提供了这个重载,编译器就可以用一个允许的用户定义转换(foo<int> const&是转换目标)来测试它。由于在本例中转换匹配,因此重载解析成功,并且得到了函数调用。

std::reference_wrapper没有operator<,所以ref_wrapper<ref_wrapper的唯一方法是通过ref_wrapper成员:

operator T& () const noexcept;
如你所知,std::string是:
typedef basic_string<char> string;

string<string的相关声明为:

template<class charT, class traits, class Allocator>
bool operator< (const basic_string<charT,traits,Allocator>& lhs, 
                const basic_string<charT,traits,Allocator>& rhs) noexcept;

对于string<string,此函数声明模板通过匹配string = basic_string<charT,traits,Allocator>来实例化,解析为charT = char,等等。

因为std::reference_wrapper(或它的任何(零)基类)不能匹配basic_string<charT,traits,Allocator>,函数声明模板不能被实例化成函数声明,也不能参与重载。

这里重要的是没有非模板operator< (string, string)原型。

显示问题的最小代码

template <typename T>
class Parametrized {};
template <typename T>
void f (Parametrized<T>);
Parametrized<int> p_i;
class Convertible {
public:
    operator Parametrized<int> ();
};
Convertible c;
int main() {
    f (p_i); // deduce template parameter (T = int)
    f (c);   // error: cannot instantiate template
}

给:

In function 'int main()':
Line 18: error: no matching function for call to 'f(Convertible&)'
<标题>标准引用

14.8.2.1从函数调用中推导模板实参[temp.演绎.call]

模板实参推导是通过将每个函数模板形参类型(称为P)与调用的相应实参类型(称为A)进行比较来完成的,如下所述。

(…)

一般来说,演绎过程试图找到模板参数值,使演绎的AA相同(在类型A如上所述转换之后)。但是,有三种情况允许有区别:

  • 如果原始P是引用类型,则推导出的A(即引用引用的类型)可以比转换后的A更符合cv。

注意std::string()<std::string()就是这种情况。

  • 转换后的A可以是另一个指针,也可以是指向成员类型的指针,这些成员类型可以通过限定转换(4.4)转换为推导出来的A

见下面的注释

  • 如果P是一个类,而P的形式为simple-template-id,则转换后的A可以是推导后的A的派生类。

这意味着在这段中:

14.8.1显式模板实参规范[temp.arg.explicit]/6

如果形参类型不包含参与模板实参演绎的模板形参,则对函数实参执行隐式转换(第4章),将其转换为相应的函数形参类型

if不应被视为当且仅当,因为它将直接与前面引用的文本矛盾。

相关文章: