【问题标题】:Why is no deduction for template parameters only used as return type?为什么不扣除模板参数只用作返回类型?
【发布时间】:2020-08-01 12:01:57
【问题描述】:

如果我在函数参数列表中不使用 tempate 参数(类型)-> 仅作为返回类型,则没有推论:

template <typename T>
T zero() { return 0; }

int main()
{
    int x = zero();
}

给予:

a.cpp:15:18: error: no matching function for call to ‘zero()’
     int x = zero();
                  ^
a.cpp:11:3: note: candidate: ‘template<class T> T zero()’
 T zero() { return 0; }
   ^~~~
a.cpp:11:3: note:   template argument deduction/substitution failed:
a.cpp:15:18: note:   couldn't deduce template parameter ‘T’
     int x = zero();

编译的唯一方法,是在尖括号中指定模板类型:

template <typename T>
T zero() { return 0; }

int main()
{
    int x = zero<int>();
}
  1. 所以我的问题是,为什么g++ 可以从模板函数的参数列表中推断出类型,而不能从中推断出来返回类型(编译器在编译main时也知道这个类型,所以它知道类型)。

  2. 当模板函数在其参数列表中使用模板类型时,为模板函数提供尖括号中的类型是任意的(因为推论)?因此,作为一个好的做法,我是否应该始终在花括号中提供类型,无论函数如何声明?

第二个问题不太可读。简而言之->无论函数声明如何,我都应该每次都使用foo&lt;T&gt;(arg, ...)(提供类型)吗?即使它可以由编译器推断出来,但我还是会提供类型以进行良好的实践?

【问题讨论】:

  • 1.是因为 C++ 是这样定义的。 2. 不是一个明确的问题,你能用更多的描述来改写它吗?
  • 当你简单地写 zero(); 时会发生什么 - 因为你可能总是在调用函数时丢弃它们的值。那么编译器应该如何推断类型呢?是不是应该推导出为void?那么这是一个错误,看起来不是很直观。

标签: c++ templates language-lawyer template-argument-deduction


【解决方案1】:

通常不可能根据函数的返回类型来推断函数。但是,如果您使用自动类型转换 c++ 功能,那么您可以实现您所需要的:

template <typename T>
T zero() { return 1; }

template <>
float zero<float>() { return 3.0f; }

struct Zero
{
    template<typename T>
    operator T()
    {
        return zero<T>();
    }
};

int main()
{
    int x = Zero();
    float y = Zero();
    return x + y;
}

首先创建临时对象 Zero(),在赋值过程中我们使用转换运算符来执行零模板函数的正确特化。

【讨论】:

  • 转换运算符是邪恶的另一个优雅证明 ;-)
【解决方案2】:

所以我的问题是,为什么 g++ 可以从模板函数的参数列表中推断出类型

GCC 遵循 C++ 标准规定的规则。

无论函数声明如何,我是否应该每次都使用 foo(arg, ...) (提供类型)?

这取决于您想要实现的目标。如果您想明确表达,请这样做。这类似于在没有模板和重载的 C 中调用 foo_T() 函数。但是,如果您希望您的代码是通用的(例如,因为它在模板本身中被调用,或者因为您希望在将来的类型更改时更容易更改),那么您宁愿避免显式编写类型。

另一个选择是使用重载而不是模板。同样,您使用哪一个取决于您想要什么以及您的用例。

最后,你也可以改用auto:

auto zero() { return 0; }

话虽如此,对于签名/接口,我认为最好在任何地方都使用显式类型除非有理由不这样做(例如,它需要是一个模板):

int zero() { return 0; }

【讨论】:

    【解决方案3】:

    问题 1

    虽然在一些琐碎的情况下修改规则可能相对容易,但通常情况并非如此。

    考虑这样的情况:

    template <A, B, C> A f(B b, C c) { ... } // #1
    int f(int a, int b) { ... } // #2
    int f(int a, double b) { ... } // #3
    

    还有这样的电话:

    double x = f(1, 2.0);  // Call #1 or #3?
    

    因此,修改规则并确保现有代码继续工作并不总是微不足道的。这样的变化很容易导致歧义,被调用的函数的静默变化......

    标准通常会避免以一种可能会导致意外无声更改的方式修改语言(或使法律代码变得模棱两可)。

    但是,如果有一种方法可以指定我们希望在特定情况下扣除返回类型,那就太好了。通过使用上下文关键字,可以定义如何处理上述情况的规则。例如,如果使用返回类型推导更喜欢与不使用返回类型推导不同的重载,则可能会发生冲突。

    问题 2

    不,如果不需要,您通常不应该提供类型。

    替代品

    虽然使用转换运算符返回类对象的解决方案可行,但在某些情况下,最简单的解决方案是更改输出参数的返回值。

    template <class T> void zero(T &t) { t = 0; }  // set_zero would be more readable
    
    int x;
    zero(x);
    

    我认为该解决方案不适合这种情况,因为以下内容更清晰:

    auto x = zero<int>();
    

    在实践中,鉴于数值类型和大多数其他类型的默认值为零,则无法从整数初始化,或者可能没有预期的结果,最好简单地写:

    int x = {};
    

    或

    int x {};
    

    【讨论】:

      【解决方案4】:

      定义语言规则来满足这种情况并非不可能,但这将是不平凡且完全没有意义的。

      当决定它的模板参数所需的所有信息都在函数中时,这个函数与模板无关。它应该只返回int,句号。如果您出于某种原因不想拼出返回类型,这就是 auto 的用途:它将执行您寻求的推论。

      另一方面,如果您希望 0 根据模板参数为您转换为不同的类型,那么这更有意义,并且您已经有了解决方案:提供模板参数(如何计算机能猜到吗?)。在这个特定的示例中,您最好只在调用站点进行转换,但可能您有一些更复杂的逻辑。


      至于你是否应该总是明确地给出模板参数,即使它们是可推导出的,我想说这在某种程度上是一种风格问题。另一方面,它看起来毫无意义且嘈杂,但另一方面它可以自我记录并确保您调用的是您认为正在调用的专业化。所以我认为这将取决于上下文。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-12-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-03-07
        • 2013-03-03
        相关资源
        最近更新 更多