C++ 中转换函数模板参数推导的含义

Implications of conversion function template argument deduction in C++

我无法理解 C++ 标准中转换函数模板参数推导规则的含义。该标准规定([temp.deduct.conv] 条款 1,N4594 中的 §14.8.2.3.1):

Template argument deduction is done by comparing the return type of the conversion function template (call it P) with the type that is required as the result of the conversion (call it A; see 8.5, 13.3.1.5, and 13.3.1.6 for the determination of that type) as described in 14.8.2.5.

其中 14.8.2.5 ([temp.deduct.type]) 是描述一般模板参数推导的部分(尽管最常见的情况是函数调用模板参数推导 [temp.deduct.call],似乎不再指向那里;曾经有过吗?)。不过,下一个条款让我感到困惑(第 2 条):

If P is a reference type, the type referred to by P is used in place of P for type deduction and for any further references to or transformations of P in the remainder of this section.

对我来说,这似乎意味着 template <class T> operator T()template <class T> operator T&() 是相同的(指定两者会导致歧义)。但在我使用过的任何编译器中都不是这种情况!例如:

struct any1 { template <typename T> operator T() { } };

struct any2 { template <typename T> operator T&() { } };

void f1(int) { }
void f2(int&) { }
void f3(int const&) { }

int main() {
  f1(any1());
  // f2(any1()); compile time error
  f3(any1());

  f1(any2());
  f2(any2());
  f3(any2());
}

Live Demo

但是如果引用被忽略,any1any2 应该有相同的行为,对吧?显然他们没有,因为 f2(any1()) 不能用 gcc 或 clang 编译,而 f2(any2()) 可以用两者编译。

下一个条款(条款 3,特别是 3.3)使事情更加混乱:

If A is not a reference type: [...] If P is a cv-qualified type, the top level cv-qualifiers of P’s type are ignored for type deduction.

这与关于删除引用的第 2 条一起,似乎暗示以下代码由于歧义不应编译:

struct any3 {
  template <typename T> operator T&() { }
  template <typename T> operator T const&() { }
};

void f1(int) { }

int main() {
  f1(any3());
}

Live Demo

然而这对 gcc 和 clang 都适用。

我错过了什么?

编辑

我应该澄清一下,clang 和 gcc 编译器处理这个问题的方式正是我对 C++ 的一般(相对高级)理解所期望的。一些评论者要求澄清我的困惑是什么(以及,含蓄地,为什么我应该关心)。我在这里的困惑完全与试图理解标准的含义有关。我需要清楚地理解这一点,因为我正在提交一篇论文,其中的代码在很大程度上依赖于这项工作以及我对它的使用是否符合标准。

函数模板的模板参数推导只是重载决策复杂过程中的一个步骤。

§13.3.1 Candidate functions and argument lists

...

7 In each case where a candidate is a function template, candidate function template specializations are generated using template argument deduction (14.8.3, 14.8.2).

对给定的函数模板执行模板参数推导就好像不存在其他函数模板一样。牢记这一点,重新阅读 §14.8.2.3 部分,您会发现您的问题属于标准的不同部分。

对所有候选模板函数进行模板参数推导后,必须根据§13.3.3的规则选择最佳可行函数。如果此时候选函数列表中存在两个或更多函数模板特化,那么最佳可行函数选择过程涉及第 14.5.6.2 节中描述的部分排序规则(我认为这部分包含对您问题的回答)。

类型推导是独立于重载解析和语义检查的步骤。

struct any1 { template <typename T> operator T() { } };

struct any2 { template <typename T> operator T&() { } };

void f1(int) { }
void f2(int&) { }
void f3(int const&) { }

int main() {
  f1(any1());
  // f2(any1()); compile time error
  f3(any1());

  f1(any2());
  f2(any2());
  f3(any2());
}

此处 f2(any1())f2(any2()) 的类型推导行为相同。两者都推导T=int。但随后 T 被替换到 原始 声明中以获得成员特化 any1::operator int()any2::operator int&()f2(any1().operator int()) 是语义错误,因为它试图将非常量左值引用函数参数绑定到右值表达式。这使得 operator int() 成为一个不可行的函数;如果any1有其他转换函数,可以通过重载决议来选择。

struct any3 {
  template <typename T> operator T&() { }
  template <typename T> operator T const&() { }
};

void f1(int) { }

int main() {
  f1(any3());
}

再次强调,这两个模板转换函数在类型推导方面的行为是相同的。两者都推导T=int。然后将该推论代入原始声明以获得 operator int&()operator int const&()。然后重载解析比较这两者。根据我对第 13 条的阅读,它们是模棱两可的,但是 gcc chooses operator int&() and clang chooses operator int const&()...

您遗漏的关键点是重载解析仍然必须发生。模板推导并不是故事的结局。分别处理您的两个示例:


To me, this seems to imply that template <class T> operator T() and template <class T> operator T&() are the same (and specifying both would result in an ambiguity). But that isn't the case in any compiler I've used!

您引用的文字表明推导T对于两个转换运算符是相同的,这是事实。但运营商本身并不相同。您还必须考虑绑定到引用的规则,这些规则在 [dcl.init.ref] 中列举。段太长无法简洁复制,但这是错误的原因

f2(any1()); // error

f2(1) 错误的原因相同:您不能将对非 const 的左值引用绑定到右值。因此,即使同时拥有两个运算符本身也不是模棱两可的:

struct X {
    template <class T> operator T();   // #1
    template <class T> operator T&();  // #2
};

f1(X{}); // error: ambiguous
f2(X{}); // ok! #1 is not viable, calls #2
f3(X{}); // ok! #2 is preferred (per [dcl.init.ref]/5.1.2)

And yet this works fine with both gcc and clang.

struct any3 {
  template <typename T> operator T&();      // #3
  template <typename T> operator T const&() // #4
};

void f1(int) { }

int main() {
  f1(any3());
}

就编译器而言,这是一个有趣的场景,因为 gcc 在这里有一个错误。两个候选人都应该有效(由于 61663,gcc 不认为 #4 有效)。 None 的决胜局适用于确定最佳可行候选人,因此在这种情况下,我们必须回退到 [temp.deduct.partial] 以确定哪个候选人更专业......在这种情况下,# 4.