Visual C++ 的两阶段模板实例化究竟Microsoft "broken"是什么?

What exactly is "broken" with Microsoft Visual C++'s two-phase template instantiation?

本文关键字:Microsoft broken 是什么 究竟 实例化 C++ 段模板 Visual      更新时间:2023-10-16

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

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

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

我将从我的"笔记本"中复制一个示例

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)

然而,微软的编译器解决了对void bar(N::S *s)的调用。这是不正确的。

这个问题在VS2015中仍然存在。

Clang项目对两阶段查找有一个很好的描述,以及各种实现的差异:http://blog.llvm.org/2009/12/dreaded-two-phase-name-lookup.html

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

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

  • gcc 4.7声称它终于正确实现了
  • CLang的目标是实现它,暴露漏洞,它在ToT上完成,并将进入3.0

我不知道为什么VC++编写者从未选择正确地实现这一点,在CLang上实现类似的行为(对于微软公司来说)暗示,延迟翻译单元末尾的模板实例化可能会带来一些性能增益(这并不意味着错误地实现查找,而是使查找变得更加困难)。此外,考虑到正确实现的明显困难,它可能更简单(也更便宜)。

我要注意的是,VC++首先也是最重要的一个商业产品。它是由满足客户的需求驱动的。

简短回答

使用/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

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

〔更新七月-2019〕这也适用于vs 2015-https://rextester.com/YOH81784,但VS2019正确显示double。根据这篇文章,MSVC支持两阶段名称查找,自VS 2017以来已修复。

既然MSVC已经实现了大部分的两阶段名称查找,我希望这篇博客文章能完全回答这个问题:两阶段名称查询出现在MSVC(VC++博客)