函数体的模板参数推导

时间:2011-01-25 21:22:53

标签: c++ templates standards iso

如果我们有这个功能模板,

template<typename T>
void f(T param) {}

然后我们可以通过以下方式调用它,

int i=0;
f<int>(i);//T=int : no need to deduce T
f(i); //T=int : deduced T from the function argument!

//likewise
sample s;
f(s); //T=sample : deduced T from the function argument!

现在考虑上述功能模板的这种变体,

template<typename TArg, typename TBody>
void g(TArg param) 
{
   TBody v=param.member;
}

现在,如果我们写的话,编译器可以推断出模板参数吗,

sample s;
g(s); //TArg=sample, TBody=int??

假设sample被定义为,

struct sample
{
   int member;
};

基本上有两个问题:

  • 编译器可以在第二个例子中推断出模板参数吗?
  • 如果不是,为什么?有什么困难吗?如果标准没有说明“从函数体中扣除模板参数”,那么是不是因为参数不能推导出来?或者它没有考虑这样的推论,以避免增加语言的复杂性?或者是什么?

我想知道你对这种扣除的看法。


编辑:

顺便说一句,如果我们编写这段代码,GCC能够推导出函数参数:

template<typename T>
void h(T p)
{
        cout << "g() " << p << endl;
        return;
}
template<typename T>
void g(T p)
{
        h(p.member); //if here GCC can deduce T for h(), then why not TBody in the previous example?
        return;
}

此示例的工作演示:http://www.ideone.com/cvXEA

上一个例子没有工作演示:http://www.ideone.com/UX038

4 个答案:

答案 0 :(得分:5)

您可能已经断定编译器不会通过检查TBody的类型来推断sample.member。这将为模板推导算法增加另一层复杂性。

模板匹配算法仅考虑函数签名,而不考虑其身体。虽然不经常使用,但简单地声明模板化函数而不提供正文是完全合法的:

template <typename T> void f(T param);

这满足了编译器。为了满足链接器,您当然还必须在某处定义函数体,并确保已提供所有必需的实例化。但是,只要在链接时可用所需的实例化,函数体就必须必须对模板函数的客户端代码可见。正文必须明确地实例化该函数,例如:

template <> void f(int param);

但这只是部分适用于您的问题,因为您可以想象如下所示的场景,其中第二个参数可以从提供的默认参数中推导出来,并且不会编译:

template<typename TArg, typename TBody>
void g(TArg param, TBody body = param.member);  // won't deduce TBody from TArg

模板匹配算法仅考虑实际类型,而不考虑类或结构的任何潜在嵌套成员类型。这会增加另一层次的复杂性,显然被认为过于复杂。算法应该停在哪里?是否还要考虑成员等等?

此外,它不是必需的,因为还有其他方法可以实现相同的意图,如下例所示。

没有什么能阻止你写作:

struct sample
{
   typedef int MemberType;
   MemberType member;
};

template<typename TArg>
void g(TArg param) 
{
   typename TArg::MemberType v = param.member;
}

sample s = { 0 };
g(s);

以获得相同的效果。


关于您在编辑后添加的示例:虽然h(p.member)似乎依赖于结构的成员,因此模板匹配算法应该失败,但它不会因为您将其分为两步过程:

  1. 在看到g(s);后,编译器会查找带有sample类型参数的任何函数(模板化或不模板化!)。在您的情况下,最佳匹配是void g(T p)此时,编译器甚至还没有查看g(T p)的正文!
  2. 现在,编译器创建了g(T p)的实例,专门用于T: sample。因此,当它看到h(p.member)时,它知道p.member属于int类型,并会尝试找到一个类型为h()的参数的函数int。您的模板函数h(T p)证明是最佳匹配。
  3. 请注意,如果您已经写过(请注意 NOT_A_member ):

    template<typename T>
    void g(T p)
    {
            h(p.NOT_A_member);
            return;
    }
    

    然后编译器仍会在第1阶段将g()视为有效匹配。然后,当sample没有名为NOT_A_member的成员时,您会收到错误。

答案 1 :(得分:0)

没有编译器可能以一致的方式实现此功能。你问的太多了。

答案 2 :(得分:0)

TBody可能不明确,因为sample可能不是唯一拥有member成员的类型。此外,如果g调用其他模板函数,则编译器无法知道可能对TBody施加的其他限制。

因此,在某些边缘情况下,理论上可以推导TBody的正确类型,通常不是。

答案 3 :(得分:0)

编译器无法对您提供的代码执行一些操作,第一个是推导出第二个模板参数TBody。首先,类型推导仅适用于编译器尝试匹配调用时函数的参数。此时,模板化函数的定义甚至没有被考虑过。

对于额外的学分,即使编译器要查看函数定义,代码TBody v = parameter.member本身也是不可推导的,因为有可能无限的数据类型可以接受parameter.member构造

现在,在第二个代码块上。要理解它,当编译器在该调用点看到函数调用g(x)时,模板编译的整个过程就开始了。编译器发现最佳候选者是模板函数template <typename T> void g( T ),并确定类型T作为重载决策的一部分。一旦编译器确定它是对模板的调用,它就会对该函数执行第一次编译传递。

在第一次传递期间,执行语法检查而不实际替换类型,因此模板参数T仍然是 any-type ,参数p是一种尚未知的类型。在第一次传递期间,代码被验证,但是依赖名称被跳过并且它们的含义被假设。当编译器看到p.member,并且p属于模板参数类型T时,它假定它将是未知类型的成员(这就是为什么如果它是一种您必须使用typename)来限定它的类型。调用h(p.member);也依赖于类型参数T并保持原样,假设一旦类型替换发生,一切都将有意义。

然后编译器确实替换了类型。在此步骤T不再是泛型类型,但它代表具体类型sample。现在,第二次传递期间的编译器会尝试填充第一次传递期间留下的间隙。当它看到p.member时,它在类型中看起来member并确定它是int并尝试使用该知识解析调用h( p.member );。因为类型T已在第二阶段之前解析,所以这相当于外部调用g(x):所有类型都是已知的,编译器只需要解析函数调用的最佳重载{ {1}}采用h类型的参数,整个过程重新开始,模板int&被认为是最佳候选者,并且......

元编程非常重要的是要理解类型演绎只能在函数的实际签名上而不是在身体上进行,而这对于初学者来说是非常重要的。在函数签名中使用h(来自boost或其他地方)作为参数或返回类型并不是巧合,但是让编译器在模板之前无法替换类型的唯一方法是被选为最佳候选者,替换失败变为实际错误(而不是SFINAE)