【问题标题】:Implications of conversion function template argument deduction in C++C++中转换函数模板参数推导的含义
【发布时间】:2016-08-25 16:37:06
【问题描述】:

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

【问题讨论】:

  • 请说明您的第一个代码中哪些是意外的,哪些是预期的。您是否期望注释行编译?其他线路的意义是什么?据我所知,注释行的Tintint prvalue 不能用于初始化int&amp;。虽然T&amp;T 情况的扣除可能相同,但转换函数将在一种情况下返回引用,而在另一种情况下则不会。这对我来说似乎很清楚。
  • @JohannesSchaub-litb 澄清了,谢谢。关键是如果P 中的引用被忽略,正如标准似乎暗示的那样,那么any1any2 应该具有相同的行为。第一个代码的工作方式与我在阅读标准之前的猜测一样(即纯右值不能绑定到左值引用,但左值引用可以),但它与我从阅读中得到的不一致标准。这就是混淆点——标准文档本身的解释
  • 对于第二种情况,我认为第二种模板更专业,因此它将被选择用于用户定义的转换序列(这是决胜局之一,否则会产生歧义) .
  • 我不明白你的困惑.. template&lt;typename T&gt; void f(T&amp;); 是否也会让你感到困惑,因为它不允许 f(1)?在类型推导过程中也忽略了引用。这并不意味着在将int 替换为T&amp; 之后它不再存在。
  • @JohannesSchaub-litb 不,这在 14.8.2.2 中是被禁止的。函数参数推导部分(14.8.2.2)没有与 14.8.2.5 中的第 2 条类似的子句

标签: c++ templates standards type-deduction


【解决方案1】:

您缺少的关键点是仍然必须进行重载解决。模板推演不是故事的结局。分别解决你的两个例子:


对我来说,这似乎意味着template &lt;class T&gt; operator T()template &lt;class T&gt; operator T&amp;() 是相同的(同时指定两者会导致歧义)。但在我使用的任何编译器中都不是这样!

您引用的文字表明 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 在这里有一个错误。两个候选人都应该是有效的(由于61663,gcc 不认为#4 有效)。没有一个决胜局适用于确定最佳可行候选人,因此在这种情况下,我们必须回退到 [temp.deduct.partial] 来确定哪个候选人更专业......在这种情况下,它是#4。

【讨论】:

  • 关于你的最后一句话,它在哪里说明 [temp.deduct.partial] 使用返回类型来决定偏序?我的印象是它只使用参数。
  • 谢谢!这样做可以,尽管我想知道在这种情况下我们是否能做到这一点——“最佳可行函数”有一个更早的条件,它着眼于转换函数的返回类型的“更好的转换顺序”。而且虽然去掉“const”的“资格调整”和“Identity”的等级相同,但“适当的子序列”条件仍然意味着它被认为更糟? 编辑:这不适用于 int 的情况,因为左值到右值的转换会丢弃 const,但请考虑 T = 类类型。
  • (这会产生相反的效果 - 至少对于类类型而言,偏爱非 const 版本。但也许这是标准中的错误或我对它的解释。)
【解决方案2】:

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

§13.3.1 候选函数和参数列表

...

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

对给定的函数模板执行模板参数推导就像不存在其他函数模板一样。记住这一点,重新阅读第 14.8.2.3 节,您会意识到您的问题属于标准的不同部分。

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

【讨论】:

    【解决方案3】:

    类型推断是与重载解析和语义检查分开的步骤。

    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 被替换到 original 声明中以获得成员特化 any1::operator int()any2::operator int&amp;()f2(any1().operator int()) 是一个语义错误,因为它试图将非 const 左值引用函数参数绑定到右值表达式。这使得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&amp;()operator int const&amp;()。然后重载决议比较这两者。根据我对第 13 条的阅读,它们是模棱两可的,但是 gcc chooses operator int&amp;() and clang chooses operator int const&amp;()...

    【讨论】:

      猜你喜欢
      • 2021-08-31
      • 1970-01-01
      • 1970-01-01
      • 2019-04-17
      • 1970-01-01
      • 2021-06-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多