3

如果我们有这个函数模板,

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

4 回答 4

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是 type int,并会尝试定位一个h()接受 type 参数的函数int。您的模板函数h(T p)结果是最好的匹配。

请注意,如果您写过(注意NOT_A_member):

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

那么编译器仍然会g()在第 1 阶段考虑有效匹配。然后当事实证明sample没有名为NOT_A_member.

于 2011-01-25T21:37:35.780 回答
0

编译器可能无法对您提供的代码做几件事,其中第一件事是推导出第二个模板参数TBody。首先,类型推导仅在编译器尝试匹配调用时应用于函数的参数。那时甚至没有查看模板化函数的定义。

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

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

在第一遍中,语法检查是在没有实际替换类型的情况下执行的,因此模板参数T仍然是任意类型,而参数p是未知类型。在第一遍中,代码被验证,但从属名称被跳过,它们的含义只是假设。当编译器看到p.member, 并且p是模板参数的类型T时,它假定它将是未知类型的成员(这就是为什么如果它是一个类型,你必须在这里用 限定它的原因typename)。该调用h(p.member);还取决于类型参数T并保持原样,假设一旦发生类型替换,一切都会变得有意义。

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

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

于 2011-01-25T23:25:16.093 回答
0

没有编译器可能以一致的方式实现此功能。你只是要求太多。

于 2011-01-25T21:33:23.193 回答
0

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

所以在某些极端情况下,理论上可以推导出 的正确类型TBody,一般情况下并非如此。

于 2011-01-25T21:36:56.027 回答