使用Microsoft Visual C++的两阶段模板实例化到底是什么"打破"?

Xeo*_*Xeo 37 c++ templates instantiation visual-c++

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

据我所知,到目前为止,MSVC++只对模板类和函数进行了基本的语法检查,并没有检查模板中使用的名称是否至少被声明了或者沿着这些行.

它是否正确?我错过了什么?

AnT*_*AnT 41

我只是从我的"笔记本"中复制一个例子

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'
}
Run Code Online (Sandbox Code Playgroud)

上面的代码应该在标准的C++编译器中编译.但是,MSVC(2005以及2010 Express)将报告错误,因为两阶段查找的实现不正确.


如果你仔细观察,问题实际上是两层的.从表面上看,显而易见的事实是,Microsoft的编译器无法对非依赖表达式执行早期(第一阶段)查找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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

这个问题仍然存在于VS2015的原始荣耀中.

  • @Simon:当发现编译器未达到标准合规性时,为向后兼容性添加命令行选项是合理的,并且默认为未来的标准合规性.这不是MS或任何编译器作者的地方接受"相对较小的可移植性损失",这有助于锁定他们的客户......这是对故意滥用的开放,以及历史表现的玩世不恭.否则,这里的意图没有比名称查找的其他100个部分更模糊了 - 为此推荐"标准"的改变是荒谬的. (16认同)
  • @Tony:实际上,如果你在包含模板之前包含了`void foo(int)`的声明,那么`foo`的"错误选择"会出现在完全兼容的编译器中.C++没有模块化的概念,因为从根本上打破了`include`的想法,因此模板/内联函数编写者不能期望*知道*将被调用的非依赖函数......并且毫无戒心的用户将会生病 - 如果这发生在一个TU而不是另一个TU,则形成程序!方案?`namespace`,`namespace`,`namespace`. (6认同)
  • @Simon:我只能不同意......在像安德烈的例子这样的代码中,模板编写者有一个foo()会做什么的想法,并希望在此基础上调用它.要替换其他一些foo()以后允许客户端代码影响模板实例化,这有两个严重的问题:1)对象可能具有相同模板/参数的不同实例,这超出了名称修改/链接器要处理的名称,2)客户端代码可以将任意功能替换为模板实现,打破概念"封装"并引入依赖关系. (4认同)
  • 在VC2010 SP1中确认了相同的行为.我不希望MS现在改变这种行为,相对较小的可移植性损失的兼容性负担太大.我实际上建议更改标准以使这样的代码(具有模糊意图)需要诊断.但那是一个不同的rabbithole ...... (3认同)
  • @TonyD 不完全是。从属名称可以改变它们的含义。这样做的问题是我们人类无法一眼看出哪些名称是相关的。 (2认同)

Sim*_*han 21

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

简短版本:两阶段查找是模板代码中名称查找的C++标准定义行为的名称.基本上,一些名称被定义为依赖(其规则有点令人困惑),在实例化模板时必须查找这些名称,并且在解析模板时必须查找独立名称.这很难实现(显然),并且让开发人员感到困惑,因此编译器往往不会将其实现为标准.要回答你的问题,看起来Visual C++会延迟所有查找,但会搜索模板上下文和实例化上下文,因此它会接受标准认为它不应该的大量代码.我不确定它是否不接受它应该接受的代码,或者更糟糕的是,它以不同的方式解释它,但它似乎是可能的.

  • 它将以不同的方式解释它 - 例如,如果在模板声明后声明更好匹配的重载,则标准定义更差的匹配,但MSVC将选择更好的匹配. (9认同)

Mat*_* M. 8

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

  • gcc的4.7索赔正确地执行它,最后
  • CLang旨在实现它,禁止错误,它在ToT上完成并将进入3.0

我不知道为什么VC++编写者从未选择正确实现这一点,在CLang上实现类似的行为(对于微软的兼容性)暗示在翻译单元末端延迟模板的实例化可能会有一些性能提升(并不意味着错误地实施查找,但使其更加困难).此外,鉴于正确实施的明显困难,它可能更简单(并且更便宜).

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

  • 在C++标准化之前,名称查找在模板中的工作原理还不太清楚.MS刚刚实现了一种方式.现在他们不会改变它,因为它们维护两种名称查找方式太多了 - 而且他们必须这样做,因为依赖于破坏行为的代码量很大(在MS内部和外部).我实际上听到了一个故事,一些人继续前进并实现了正确的名称查找,但它打破了很多代码,以至于他们从未整合了这一变化. (3认同)
  • 微软工程师[已指出](http://blogs.msdn.com/b/somasegar/archive/2014/05/28/first-preview-of-visual-studio-quot-14-quot-available-now.aspx )他们计划最终正确地实现两阶段查找(参见VC++一致性更新表).他们很难实现此功能,因为他们的编译器不使用完整的AST. (2认同)

mar*_*inj 5

简短的回答

使用/ 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;
}
Run Code Online (Sandbox Code Playgroud)

将打印:

type of a: int
Run Code Online (Sandbox Code Playgroud)

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