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

时间:2016-08-25 16:37:07

标签: c++ templates standards type-deduction

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

  

模板参数推导是通过将转换函数模板的返回类型(称为P)与转换​​结果所需的类型进行比较来完成的(将其称为A;请参见8.5,13.3.1.5和13.3。 1.6确定该类型)如14.8.2.5所述。

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

  

如果P是参考类型,则使用P所引用的类型代替P来进行类型推导,以及在本节其余部分中对P的任何进一步引用或转换。

对我来说,这似乎意味着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)使事情进一步混淆:

  

如果A不是引用类型:[...]如果P是cv限定类型,则类型推导将忽略P类型的顶级cv限定符。

这与关于删除引用的第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 ++的一般(相对高级)理解的期望。一些评论者要求澄清我的困惑(并且隐含地说,为什么我应该关心)。我在这里的困惑与试图理解标准的含义完全相关。我需要清楚地理解这一点,因为我提交的论文的代码很大程度上依赖于这项工作以及我对它的使用是否符合标准。

3 个答案:

答案 0 :(得分:3)

您缺少的关键点是仍然必须进行重载解析。模板演绎不是故事的结尾。分别处理两个示例:

  

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

您引用的文字表明扣除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)
  

然而这对gcc和clang都很好。

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

void f1(int) { }

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

就编译器而言,这是一个有趣的场景,因为gcc在这里有一个bug。两个候选人都应该有效(gcc不认为#4因61663而有效)。没有一个破坏者适用于确定最佳可行候选人,所以在这种情况下我们必须回到[temp.deduct.partial]来确定哪个候选人更专业化......在这种情况下,#4。

答案 1 :(得分:2)

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

  

§13.3.1候选函数和参数列表

     

...

     

7在候选者是函数模板,候选函数的每种情况下   使用模板参数推导生成模板特化   (14.8.3,14.8.2)。

对给定的函数模板执行模板参数推导,就像没有其他函数模板一样。请仔细阅读§14.8.2.3部分,您将意识到您的问题属于标准的不同部分。

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

答案 2 :(得分: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())是语义错误,因为它尝试将非const左值引用函数参数绑定到rvalue表达式。这使得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&() ......