【问题标题】:How are template definitions matched to template declarations?模板定义如何与模板声明匹配?
【发布时间】:2012-03-08 07:24:38
【问题描述】:

模板声明与模板定义的匹配程度如何?我在标准中发现了一些关于 template-ids 引用相同函数的文本,如果“他们的 template-names [...] 引用相同的模板并且 [.. .]" (14.4 [temp.type] p1) 但我找不到 template-names 的定义或当 template-names 引用同一模板时。我不确定我是否走在正确的轨道上,因为我还没有很好地破译语法以判断 template-id 是否是模板定义/声明的一部分,或者只是一个模板的使用。

例如,下面的程序可以正常工作。

#include <iostream>

template<typename T>
T foo(T t);

int main() {
    foo(1);
}

template<typename T>
T foo(T t)
{ std::cout << "A\n"; return 0; }

如果我更改在模板定义中使用模板参数的方式,名称显然不再引用同一个模板,并且链接失败。

#include <iostream>

template<typename T>
T foo(T t);

int main() {
    foo(1);
}

template<typename T>
int foo(T t) { std::cout << "A\n"; return 0; }

// or

template<typename T>
struct identity {
    typedef T type;
};

template<typename T>
typename identity<T>::type
foo(T t) { std::cout << "A\n"; return 0; }

接下来,如果我将模板定义移动到另一个翻译单元,对于我的 C++(MSVC 11 测试版)实现,无论我如何说类型,程序都能正常工作。

//main.cpp

template<typename T>
T foo(T t);

int main() {
    foo(1);
}

//definition.cpp
#include <iostream>

template<typename T>
struct identity {
    typedef T type;
};

template<typename T>
typename identity<T>::type
foo(T t) { std::cout << "A\n"; return 0; }

template int foo<int>(int);

//definition.cpp
#include <iostream>

template<typename T>
int foo(T t) { std::cout << "A\n"; return 0; }

template int foo<int>(int);

或者即使定义根本不是模板:

//definition.cpp
#include <iostream>

int foo(T t) { std::cout << "A\n"; return 0; }

显然链接是成功的,因为无论为创建符号而实例化的模板如何,签名/损坏的名称都是相同的。我认为这种未定义的行为是因为我违反了:

§ 14.1 [临时] p6

函数模板、类模板的成员函数或静态 类模板的数据成员应在每个翻译中定义 它被隐式实例化的单元(14.7.1),除非 相应的特化在 (14.7.2) 中显式实例化 一些翻译单元;不需要诊断。

然后说我试图通过将模板的定义放在第二个翻译单元中来满足这些要求,并在两个位置之一包含一个显式实例化:

#include <iostream>

template<typename T>
T foo(T t) { std::cout << "A\n"; return 0; }

// Location 1    

template<typename T>
int foo(int t) { std::cout << "B\n"; return 0; }

// Location 2

消除显式实例化所指模板的歧义的规则是什么?将其放在位置 1 会导致正确的模板被实例化,并且该定义将在最终程序中使用,而将其放在位置 2 会实例化另一个模板,并导致我认为在上述 14.1 p6 下的未定义行为。

另一方面,两个模板定义的隐式实例化无论如何都会选择第一个模板,因此在这些情况下,消除模板歧义的规则似乎有所不同:

#include <iostream>

template<typename T>
T foo(T t) { std::cout << "A\n"; return 0; }

template<typename T>
int foo(int t) { std::cout << "B\n"; return 0; }

int main() {
    foo(1); // prints "A"
}

出现此问题的原因与this question 有关,提问者在其中发现单个前向声明

template<typename T>
T CastScriptVarConst(const ScriptVar_t& s);

不能作为多个模板定义的声明:

template<typename T>
typename std::enable_if<GetType<T>::value < SVT_BASEOBJ,T>::type
CastScriptVarConst(const ScriptVar_t& s) {
    return (T) s;
}

template<typename T>
typename std::enable_if<!(GetType<T>::value < SVT_BASEOBJ)
                        && std::is_base_of<CustomVar,T>::value,T>::type
CastScriptVarConst(const ScriptVar_t& s) {
    return *s.as<T>();
}

我想更好地理解模板定义和声明之间的关系。

【问题讨论】:

  • 为完整的故事挑选一份“C++ 模板:完整指南”。是的,它比标准更容易消化......实际上很多;)

标签: c++ templates


【解决方案1】:

好的,让我们从头开始。模板的“模板名称”是被模板化的函数或类的实际名称;也就是说,在

template<class T> T foo(T t);

foo 是模板名称。对于函数模板,判断它们是否相同的规则很长,在14.5.5.1“函数模板重载”中有描述。该部分的第 6 段(我在此处引用 C++03,因此 C++11 中的措辞和段落编号可能已更改)定义了术语 equivalentfunctionally equivalent,当应用于涉及模板参数的表达式时。

简而言之,等价表达式除了可能具有不同的模板参数名称外是相同的,而功能上等价表达式如果碰巧求值相同,则它们是相同的事物。例如,前两个f 声明等效,但第三个仅功能等效与其他两个:-

template<int A, int B>
void f(array<A + B>);
template<int T1, int T2>
void f(array<T1 + T2>);
template<int A, int B>
void f(array< mpl::plus< mpl::int<A>, mpl::int<B> >::value >);

第 7 段继续将这两个定义扩展到整个函数模板。匹配的两个函数模板(在名称、范围和模板参数列表中)如果它们也具有等效的返回类型和参数类型,则它们是等效的,或者如果它们仅具有功能等效的返回类型和参数类型,则它们是功能等效的。查看您的第二个示例,这两个函数仅在功能上等效:-

template<typename T>
T foo(T t);

template<typename T>
typename identity<T>::type foo(T t);

第 7 段以可怕的警告结束,“如果程序包含功能等效但不等效的函数模板声明,则程序格式错误;不需要诊断。”因此,您的第二个示例是无效的 C++。检测这样的错误需要在二进制文件中使用 AST 来注释函数模板的每个声明和定义,该 AST 描述了每个参数和返回类型来自的模板表达式,这就是标准不需要实现来检测它的原因。 MSVC 有理由按照您的意图编译您的第三个示例,但也有理由中断。

继续显式实例化,重要的部分是第 14.7 节,“模板实例化和专业化”。第 5 段不允许以下所有内容:

  • 多次显式实例化模板;
  • 显式实例化和显式特化同一个模板;
  • 为同一组参数多次显式特化模板。

同样,“不需要诊断”,因为它很难检测到。

因此,为了扩展您的显式实例化示例,以下代码违反了第二条规则并且是非法的:-

/* Template definition. */
template<typename T>
T foo(T t)
{ ... }

/* Specialization, OK in itself. */
template< >
int foo(int t)
{ ... }

/* Explicit instantiation, OK in itself. */
template< >
int foo(int t);

无论显式特化和显式实例化的位置如何,这都是非法的,但当然因为不需要诊断,您可能会在某些编译器上得到有用的结果。还要注意显式实例化和显式特化之间的区别。以下示例格式错误,因为它声明了一个显式特化但没有定义它:-

template<typename T>
T f(T f)
{ ... }

template< >
int f(int);

void g(void)
{ f(3); }

但是这个例子是结构良好的,因为它有一个明确的实例化:-

template<typename T>
T f(T f)
{ ... }

template f(int);

void g(void)
{ f(3); }

&lt; &gt; 与众不同。还要注意的是,即使您确实定义了显式特化,也必须在 在使用它之前,否则编译器可能已经为该模板生成了一个隐式实例化。这在 14.7.3“显式专业化”第 6 段中,就在您正在阅读的位置下方,同样,不需要诊断。为了适应同样的例子,这是不正确的:-

template<typename T>
T f(T f)
{ ... }

void g(void)
{ f(3); } // Implicitly specializes int f(int)

template< >
int f(int) // Too late for an explicit specialization
{ ... }

如果你还不够困惑,看看你的最后一个例子:-

template<typename T>
T foo(T t) { ... }

template<typename T>
int foo(int t) { ... }

foo 的第二个定义不是第一个定义的特化。它必须是 template&lt; &gt; int foo(int) 才能成为 template&lt;typename T&gt; T foo(T) 的专业化。但这没关系:函数重载是允许的,函数模板和普通函数之间是允许的。 foo(3) 形式的调用将始终使用第一个定义,因为它的模板形参T 可以从实参类型推导出来。第二个定义不允许从实参类型推导出其模板形参。只有显式指定T才能达到第二个定义,并且只有当调用与第一个定义没有歧义时:-

f<int>(3); // ambiguous
f<string>(3); // can only be the second one

对函数模板做重载解析的整个过程太长了,这里就不一一介绍了。如果您有兴趣并提出更多问题,请阅读第 14.8.3 节:-)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-04-25
    • 2016-05-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-11-19
    相关资源
    最近更新 更多