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

时间:2011-12-14 23:08:31

标签: c++ templates implicit-conversion template-deduction reference-wrapper

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

int a = 5, b = 7;

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

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

(正如question linked below所示,这可以是一种有用的方法来生成现有集合的备用视图,可以随意重新排列,而不会产生完整副本的成本,以及使用原始集合维护更新完整性。)


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

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

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

return t1 < t2;  // ERROR

我的解决方法是在this answer *中定义谓词;但我的问题是:

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

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

2 个答案:

答案 0 :(得分:7)

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

  

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


§14.8.3 [temp.over] p1

  

[...]写入对该名称的调用(显式或隐式使用运算符)   符号),模板参数推导(14.8.2)和任何显式模板参数(14.3)的检查是为每个函数模板执行的,以找到可与该函数模板一起使用来实例化函数模板的模板参数值(如果有的话)可以使用调用参数调用的特化。

§14.8.2.1 [temp.deduct.call] p4

  

[注意:在14.8.1中指定,隐式转换将在函数参数上执行,将其转换为相应的类型函数参数如果参数不包含参与模板参数推断的模板参数 。 [...] -end note ]

§14.8.1 [temp.arg.explicit] p6

  

如果参数类型不包含参与模板参数推断的 template-parameters ,则将对函数参数执行隐式转换(第4节)以将其转换为相应函数参数的类型。 [注意:模板参数如果明确指定,则不参与模板参数推断。 [...] -end note ]

由于std::basic_string取决于推导出的模板参数(CharTTraits),因此不允许转换。


这是一种鸡和蛋的问题。要推断模板参数,它需要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&作为转换目标)来测试它。由于转换匹配在这种情况下,重载解析成功,我们得到了函数调用。

答案 1 :(得分:6)

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
}

Gives

In function 'int main()':
Line 18: error: no matching function for call to 'f(Convertible&)'

标准引文

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

  

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

(...)

  

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

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

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

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

见下面的评论。

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

注释

这意味着在本段中:

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

  

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

if 不应被视为当且仅当,因为它会直接与之前引用的文字相矛盾。