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

Ker*_* SB 17 c++ templates implicit-conversion reference-wrapper template-argument-deduction

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

int a = 5, b = 7;

std::reference_wrapper<int> p(a), q(b);   // or "auto p = std::ref(a)"
Run Code Online (Sandbox Code Playgroud)

现在我可以很容易地说if (p < q),因为引用包装器已转换为其包装类型.一切都很开心,我可以处理一组参考包装器,就像它们是原始对象一样.

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


但是,对于某些类,这不起作用:

std::string s1 = "hello", s2 = "world";

std::reference_wrapper<std::string> t1(s1), t2(s2);

return t1 < t2;  // ERROR
Run Code Online (Sandbox Code Playgroud)

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

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

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

Xeo*_*Xeo 8

编辑:将我的猜测移到了底部,这里是规范性文本,为什么这不起作用.TL; DR版本:

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


§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;
}
Run Code Online (Sandbox Code Playgroud)

如果我们没有为实例化提供重载int,则推断失败.如果我们提供那个重载,那么编译器可以使用一个允许的用户定义转换(foo<int> const&作为转换目标)来测试它.由于转换匹配在这种情况下,重载解析成功,我们得到了函数调用.


cur*_*guy 6

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

operator T& () const noexcept;
Run Code Online (Sandbox Code Playgroud)

如您所知,std::string是:

typedef basic_string<char> string;
Run Code Online (Sandbox Code Playgroud)

相关声明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;
Run Code Online (Sandbox Code Playgroud)

对于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
}
Run Code Online (Sandbox Code Playgroud)

:

In function 'int main()':
Line 18: error: no matching function for call to 'f(Convertible&)'
Run Code Online (Sandbox Code Playgroud)

标准引文

14.8.2.1从函数调用中减去模板参数[temp.deduct.call]

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

(......)

通常,演绎过程会尝试查找模板参数值,这些参数值将使推导出A相同A(在A如上所述变换类型之后).但是,有三种情况可以产生差异:

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

请注意,情况就是如此std::string()<std::string().

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

见下面的评论.

  • 如果P是一个类并且P具有simple-template-id形式,那么转换后的A可以是推导出的派生类A.

评论

这暗示在本段中:

14.8.1显式模板参数说明[temp.arg.explicit]/6

如果参数类型不包含参与模板参数推导的模板参数,则将对函数参数执行隐式转换(第4节)以将其转换为相应函数参数的类型.

如果不应该被视为一个当且仅当,因为这将直接违背先前引用的文本.