【问题标题】:What exactly is "broken" with Microsoft Visual C++'s two-phase template instantiation?Microsoft Visual C++ 的两阶段模板实例化究竟是什么“破坏”了?
【发布时间】:2011-09-10 12:26:45
【问题描述】:

阅读关于 SO 的问题、cmets 和答案,我一直听说 MSVC 没有正确实现两阶段模板查找/实例化。

据我目前了解,MSVC++ 仅对模板类和函数进行基本语法检查,并没有检查模板中使用的名称是否至少已声明或类似的内容。

这是正确的吗?我错过了什么?

【问题讨论】:

  • 我没有这方面的参考,但我相信现在已经实现了,可能在 VC 2005 或 2008 中。
  • @Simon :我不这么认为。
  • @ildjarn:我收回我的声明!模板编译得到了显着改进,但特别是两阶段查找显然仍然过于松懈。从定义来看,我认为 broken 可能是一个过于强大的术语。
  • 我对 VC2010 的体验给我的印象是编译器在第一阶段查找名称的工作比所需的要少得多。至少我发现了更多的 gcc 错误,VC 跳过了。
  • 你可能会发现最近提交给 Visual Studio 的 bug 很有趣:MSVC 需要命名空间,即使它被提升 - connect.microsoft.com/VisualStudio/feedback/details/715626 或许,如果更多人投票,它会尽快修复。

标签: c++ templates visual-c++ instantiation


【解决方案1】:

我只是从我的"notebook" 中复制一个示例

int foo(void*);

template<typename T> struct S {
  S() { int i = foo(0); }
  // A standard-compliant compiler is supposed to 
  // resolve the 'foo(0)' call here (i.e. early) and 
  // bind it to 'foo(void*)'
};

void foo(int);

int main() {
  S<int> s;
  // VS2005 will resolve the 'foo(0)' call here (i.e. 
  // late, during instantiation of 'S::S()') and
  // bind it to 'foo(int)', reporting an error in the 
  // initialization of 'i'
}

以上代码应该在标准 C++ 编译器中编译。但是,MSVC(2005 和 2010 Express)会因为两阶段查找的错误实现而报错。


如果您仔细观察,问题实际上是两层的。从表面上看,微软的编译器无法对非依赖表达式foo(0) 执行早期(第一阶段)查找是一个明显的事实。但它之后所做的并不真正表现为第二个查找阶段的正确实现。

语言规范明确指出,在第二个查找阶段,只有 ADL 命名的命名空间通过在定义点和实例化点之间累积的额外声明进行扩展。同时,非 ADL 查找(即普通的非限定名称查找)在第二阶段进行扩展 - 它仍然会看到那些并且仅看到在第一阶段可见的那些声明。

这意味着在上面的示例中,编译器也不应该在第二阶段看到void foo(int)。换句话说,MSVC 的行为不能仅仅用“MSVC 将所有查找推迟到第二阶段”来描述。 MSVC 实现的也不是第二阶段的正确实现。

为了更好地说明问题,请考虑以下示例

namespace N {
  struct S {};
}

void bar(void *) {}

template <typename T> void foo(T *t) {
  bar(t);
}

void bar(N::S *s) {}

int main() {
  N::S s;
  foo(&s);
}

请注意,即使模板定义中的bar(t) 调用是在第二个查找阶段解析的相关 表达式,它仍应解析为void bar(void *)。在这种情况下,ADL 不会帮助编译器找到void bar(N::S *s),而常规的非限定查找不应该在第二阶段得到“扩展”,因此也不应该看到void bar(N::S *s)

然而,Microsoft 的编译器将调用解析为void bar(N::S *s)。这是不正确的。

问题在 VS2015 中仍然存在。

【讨论】:

  • 在 VC2010 SP1 中确认了相同的行为。我不希望 MS 现在改变这种行为,对于相对较小的可移植性损失来说,兼容性负担太大了。我实际上建议更改标准以使这样的代码(意图不明确)需要诊断。但那是一个不同的兔子洞......
  • @Simon:当发现编译器不符合标准时,添加命令行选项以实现向后兼容性并默认为未来的标准符合性是合理的。接受“相对较小的可移植性损失”有助于锁定他们的客户......这不是 MS 或任何编译器编写者的地方......这很容易被故意滥用,历史表明经常被愤世嫉俗地接受。否则,这里的意图没有什么比名称查找的 100 个其他部分更模棱两可了——为此“建议”对标准进行更改是荒谬的。
  • @Simon:我只能不同意……在像 Andrey 的示例这样的代码中,模板编写者对 foo() 将做什么有一个概念,并希望在此基础上调用它。稍后替换一些其他的 foo() 允许客户端代码影响模板实例化,这有两个严重的问题:1)对象可能对相同的模板/参数有不同的实例化,这超出了名称 mangling/linker 的含义, 2) 客户端代码可以将任意功能替换到模板实现中,打破概念上的“封装”并引入依赖关系。
  • @Tony:实际上,如果您在包含模板之前包含void foo(int) 的声明,那么完全兼容的编译器会发生foo 的“错误选择”。 C++ 没有模块化的概念,因为从根本上破坏了include 的想法,因此模板/内联函数编写者不能期望知道将被调用的非依赖函数......以及毫无戒心的用户如果这种情况发生在一个 TU 而不是另一个 TU,则将有一个格式错误的程序!解决方案 ? namespace, namespace, namespace.
  • @TonyD 不完全是。从属名称可以改变它们的含义。这样做的问题是,我们人类无法一眼看出哪些名称是依赖的。
【解决方案2】:

Clang 项目对两阶段查找有很好的描述,不同的实现差异是什么:http://blog.llvm.org/2009/12/dreaded-two-phase-name-lookup.html

短版:两阶段查找是 C++ 标准定义的模板代码中名称查找行为的名称。基本上,有些名字被定义为依赖(规则有点混乱),这些名字必须在实例化模板时查找,独立的名字解析模板时必须查找。这既难以实现(显然),也让开发人员感到困惑,因此编译器往往不会按照标准实现它。要回答您的问题,Visual C++ 似乎会延迟所有查找,但会同时搜索模板上下文和实例化上下文,因此它接受了许多标准规定不应执行的代码。我不确定它是否接受它应该的代码,或者更糟的是,它的解释不同,但似乎有可能。

【讨论】:

  • 它会以不同的方式解释它——例如,如果你在模板声明之后声明了更好匹配的重载,标准定义了更差的匹配,但 MSVC 会选择更好的匹配。
【解决方案3】:

从历史上看,gcc 也没有正确实现两阶段名称查找。显然很难达到,或者至少没有太多的动力......

  • gcc 4.7 终于声明了implement it correctly
  • CLang 旨在实现它,暴露错误,它在 ToT 上完成,并将进入 3.0

我不知道为什么 VC++ 编写者从不选择正确实现这一点,在 CLang 上实现类似行为(为了微软兼容性)暗示可能有一些 performance gain 来延迟模板的实例化末尾翻译单元(这并不意味着不正确地执行查找,而是使其更加困难)。此外,鉴于正确实现的明显困难,它可能更简单(也更便宜)。

我会注意到 VC++ 首先是一个商业产品。它的驱动力是满足客户的需求。

【讨论】:

  • 在 C++ 标准化之前,名称查找在模板中的工作方式还不太清楚。 MS只是实现了一种方式。现在他们不会更改它,因为维护这两种名称查找方式对他们来说工作量太大——而且他们必须这样做,因为依赖于损坏行为的代码量惊人(在 MS 内部和外部)。我实际上听说过一个故事,有人继续实施正确的名称查找,但它破坏了太多代码,以至于他们从未集成更改。
  • @SebastianRedl: Backward compatibility hurts kitten :( 但我明白他们不愿意破坏他们的客户代码。
  • Microsoft 工程师有indicated 表示他们计划最终正确实现两阶段查找(请参阅 VC++ 一致性更新表)。这个特性对他们来说很难实现,因为他们的编译器不使用完整的 AST。
【解决方案4】:

简短回答

使用 /Za 禁用语言扩展

更长的答案

我最近正在调查这个问题,很惊讶在 VS 2013 下,标准 [temp.dep]p3 中的以下示例会产生错误的结果:

typedef double A;
template<class T> class B {
public:
    typedef int A;
};
template<class T> struct X : B<T> {
public:
    A a;
};

int main()
{
    X<int> x;
    std::cout << "type of a: " << typeid(x.a).name() << std::endl;
}

将打印:

type of a: int

虽然它应该打印double。使 VS 符合标准的解决方案是禁用语言扩展(选项 /Za),现在 x.a 的类型将解析为 double,其他使用基类中的依赖名称的情况也将符合标准。我不确定这是否会启用两阶段查找。

[2019 年 7 月更新] VS 2015 也是如此 - https://rextester.com/YOH81784,但 VS2019 正确显示 double。根据这篇文章Two-phase name lookup support comes to MSVC,自VS 2017以来已修复。

【讨论】:

  • /Za 在这种情况下没有帮助,在 VS2015 中测试
  • 吹毛求疵:“它应该打印双倍”不,它应该打印typeid(double).name(),不一定是double
【解决方案5】:

现在 MSVC 已经实现了大部分的两阶段名称查找,我希望这篇博文能够完全回答这个问题:Two-phase name lookup comes to MSVC (VC++ blog)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-12-31
    • 2021-08-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-29
    • 1970-01-01
    相关资源
    最近更新 更多