17

假设我有一些 type 的对象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

我的解决方法是定义一个谓词,如this answer *; 但我的问题是:

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

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

4

2 回答 2

9

编辑:将我的猜测移到底部,这是规范文本为什么这不起作用。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;
}

如果我们不为 上的实例化提供重载int,则推导失败。如果我们提供该重载,则编译器可以使用允许的用户定义转换(foo<int> const&作为转换目标)对其进行测试。由于在这种情况下转换匹配,重载解析成功,我们得到了函数调用。

于 2011-12-14T23:24:42.693 回答
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
}

给出

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(即引用所引用的类型)可以比转换后的 cv 更合格A

请注意,这是std::string()<std::string().

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

请参阅下面的评论。

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

评论

这意味着在本段中:

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

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

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

于 2011-12-14T23:27:19.917 回答