【问题标题】:What does a compiler check for uninstantiated template code?编译器检查未实例化的模板代码是什么?
【发布时间】:2024-04-27 03:50:02
【问题描述】:

例如,以下代码段使用 gcc-4.9 和 clang-602 编译

class Base                                                                      
{                                                                               
public:                                                                         
    static void foo() {}                                                        
    void badfoo(int i) {}                                                       
};                                                                              

template <typename T>                                                           
class Derived : public Base                                                     
{                                                                               
public:                                                                         
    void bar() { Base::foo(); }                                                 
    void badbar() { Base::badfoo(); }  // compiles ok
    //static void badbar() { Base::badfoo(); }  // compile error                                                                                    
    //void worsebar() { Base::nonexist(); }  // compile error                                   
};                                                                              

int main()                                                                      
{                                                                               
    return 0;                                                                   
}  

但是注释掉的行不会编译。

我的问题是:

  1. 为什么badbar() 编译但worsebar() 不编译?

  2. 如果我将badbar() 更改为静态,它也不会编译,无论base::badfoo 是否为静态。

  3. 标准是否说明在这种情况下应该检查什么? gcc4.4居然连badbar()都拒绝编译。

更新:

许多答案已经解释了问题 1,但似乎编译器有时也会加倍努力检查过载,它发生在 gcc 4.4.3 和 4.8.2,但不是 4.7.2 和 4.9.1 .

问题 2:正如 Marco A. 指出的那样,clang 不会编译,但 gcc4.9 仍然可以通过。但是,gcc4.2 和 gcc4.4 都拒绝了代码,他们抱怨的错误是“没有匹配的函数”而不是“在没有对象的情况下调用非静态成员”。这个问题似乎没有确凿的答案,所以我按照 Daniel Frey 的建议添加了语言律师标签。

更多更新:

我认为问题 2 的答案现在很清楚:添加静态声明是否会改变诊断取决于编译器。它因编译器和同一编译器的不同版本而异。语言律师没有出现,我将接受 Daniel Frey 的回答,因为它最好地解释了第一个问题。但 Marco A. 和 Hadi Brais 的回答也值得一读。

【问题讨论】:

  • 为什么 worsebar 编译?它试图调用一个不存在的函数。
  • @JoachimPileborg 但Base::badfoo() 也是一个不存在的函数。那为什么会编译呢?
  • 请编辑您的问题以显示您收到的实际错误,包括警告,完整和未编辑。
  • @juanchopanza 好的。 Joachims 的回答解释了除了 Base::badfoo(); 之外的所有内容,这确实很有趣,为什么它会编译。
  • 对于静态函数,静态成员函数只能调用其他静态成员函数,没有明确的对象实例。

标签: c++ templates gcc clang language-lawyer


【解决方案1】:

该标准只要求名称查找发生在第 1 阶段,即第一次解析模板时。这会在badbar 中出现badfoo,这就是代码编译的原因。那时编译器不需要做重载决议。

badbar 被实例化时,重载解决方案(总是作为一个单独的步骤名称查找之后发生)然后在第2 阶段执行 - 在您的示例中不是这种情况。这个原则可以在

中找到

3.4 名称查找 [basic.lookup]

1 名称查找规则统一适用于所有名称(包括 typedef-names (7.1.3)、namespace-names (7.3 ) 和 class-names (9.1)),只要语法允许在特定规则讨论的上下文中使用此类名称。名称查找将名称的使用与该名称的声明 (3.1) 相关联。名称查找应找到名称的明确声明(见 10.2)。如果名称查找发现名称是函数名称,则名称查找可能会将多个声明与名称相关联;据说这些声明形成了一组重载函数(13.1)。 重载解析 (13.3) 在名称查找成功后进行。 仅在名称查找和函数重载解析(如果适用)成功后才考虑访问规则(第 11 条)。只有在名称查找、函数重载解析(如果适用)和访问检查成功之后,名称声明中引入的属性才能在表达式处理中进一步使用(第 5 条)。

(强调我的)

因此,我会说编译器可以正确接受代码,尽管我不确定他们是否需要这样做。

要查看代码being rejected,您需要实例化badbar

【讨论】:

  • 您的回答解释了 badbar 和 badbar 之间的区别,但为什么 badbar 如果声明为静态也会失败?
  • @swang 对我来说,它被 Clang 拒绝但被 GCC 接受。正如我在回答中所写,编译器可能不需要 来延迟重载决议。我还没有找到任何关于这件事的权威,所以两个编译器都可能是对的。
  • 那么在静态的情况下,编译器可能决定尽早查看重载决议并拒绝代码?如果没有人能找到任何结论性的解释,那么它可能取决于实现?
  • @swang 至少在我看来是这样。如果您愿意,请致电language-lawyer,可能值得一试。或者提出一个 DR,看看标准的人对此有什么看法。
  • 我怎么称呼它,我只是将那个标签添加到这个问题中吗?
【解决方案2】:

考虑 [temp.res]/8:

如果无法为模板生成有效的特化,并且 模板未实例化,模板格式不正确,否 需要诊断。

这(特别是“不需要诊断”位)使任何编译器的行为都符合worsebar。此类代码的实现差异只是 QoI 问题 - 常见的编译器会进行一些分析并会抱怨。确切的时间很难说,你应该准备好在升级或切换你的实现时回到模板代码。

【讨论】:

  • 我认为 worsebar 违反了 [temp.res]/p10,这确实需要诊断。
  • @T.C.在定义时间?
  • 是的。它说对于非依赖名称,“该名称的声明(或声明集)应在该名称出现在模板定义中的点的范围内”。没有 NDR。
  • @T.C.那实际上是有缺陷的;不需要诊断([temp.res]/8 旨在涵盖 /10)。 Mike Miller 指出了这一点。我会尽力澄清一下。
【解决方案3】:

为了清楚起见.. 这个版本的代码在 clang 和 gcc 上编译得很好

class Base                                                                      
{                                                                               
public:                                                                         
    static void foo() {}                                                        
    void badfoo(int a) {}                                                       
};                                                                              

template <typename T>                                                           
class Derived : public Base                                                     
{                                                                               
public:                                                                         
    void bar() { Base::foo(); }                                                 
    void badbar() { Base::badfoo(); }   
}; 

因为

[temp.res]/p8

如果没有有效的专业化可以 为模板生成,并且该模板未实例化,模板格式错误,无诊断 必填。

gcc 和 clang 不需要诊断此问题。这也属于与上述相同的情况(clang 发出错误,gcc 不会)

class Base                                                                      
{                                                                               
public:                                                                         
    static void foo() {}                                                        
    void badfoo(int a) {}                                                   
};                                                                              

template <typename T>                                                           
class Derived : public Base                                                     
{                                                                               
public:                                                                         
    void bar() { Base::foo(); }                                                 
    static void badbar() { Base::badfoo(); } 
}; 

的情况
void worsebar() { Base::nonexist(); }

不同,因为 它违反了名称查找 [temp.res]/p9

在查找模板定义中使用的名称声明时,通常的查找规则 (3.4.1, 3.4.2) 用于非依赖名称

[temp.res]/p10

如果名称不依赖于模板参数(如 14.6.2 中所定义),则声明(或声明集) 因为该名称应在该名称出现在模板定义中的位置范围内

免责声明:以上所有内容均不适用于 MSVC,它很乐意将所有这些内容推迟到查找的第二阶段。


编辑:

在这种情况下

class Base                                                                      
{                                                                               
public:                                                                         
    static void foo() {}                                                        
    void badfoo(int i) {}                                                       
};                                                                              

template <typename T>                                                           
class Derived : public Base                                                     
{                                                                               
public:                                                                         
    static void badbar() { Base::badfoo(); }  // static function

clang triggers an errorgcc doesn't。这属于第一种情况,因为名称查找成功但 clang 执行了额外的检查:因为 badfoo 是一个成员函数,它试图构造一个有效的隐式成员引用表达式。然后它会捕捉到成员函数正在从静态函数中被隐式调用的事实,并检测上下文不匹配。此时此诊断完全取决于编译器(在实例化的情况下不会)。

【讨论】:

    【解决方案4】:

    在实例化任何类型或发出任何代码之前,编译器会逐步构建一个包含已声明的所有符号的表。如果使用了未声明的符号,则会发出错误。这就是 badbar 无法编译的原因。另一方面, badfoo 已被声明,所以 badbar 编译。在编译过程的早期阶段,编译器不会检查对 badfoo 的调用是否实际上与声明的 badfoo 匹配。

    由于 Derived 类型没有在代码中的任何地方实例化,编译器不会发出任何关于它的代码。特别是,badbar 将被忽略。

    现在,当您声明 Derived 的实例(例如 Derived)但不使用其任何成员时,编译器将仅使用已使用的成员创建一个类型并忽略其他成员。不过,关于 badbar 没有错误。

    但是,当声明 Derived 的实例并调用 badbar 时,需要实例化 badbar 方法,因此编译器将创建一个带有 badbar 的类型并对其进行编译。这一次,编译器注意到 badfoo 并没有真正声明,因此会发出错误。

    此行为记录在 C++ 标准的第 14.7.1 节中。

    除非类模板或成员模板的成员已被显式实例化或显式特化,否则当在需要成员定义存在的上下文中引用特化时,成员的特化将被隐式实例化。

    最后,如果 badbar 是静态的并且被编译器实例化(因为它已被使用),那么编译器将发出 badfoo 不存在的错误。现在,如果您将整数参数传递给 badfoo,将发出另一个错误,指示静态方法无法访问实例成员,因为首先没有实例。

    编辑

    编译器没有义务不报告未实例化模板类型中的语义错误。该标准只是说它不必,但它可以。关于在哪里划清界限还有待商榷。参见this关于clang中相关问题的讨论:

    我们分析哪些未实例化的模板?出于性能原因,我认为我们不应该分析所有未实例化的模板,因为我们可能会发现自己重复分析了大部分 Boost 和 STL 等。

    因此,未实例化的模板分析会随着不同版本的 clang 和 gcc 以不同的方式发生变化。但同样,按照标准:There's no requirement to report errors in uninstantiated templates, of course.

    【讨论】:

    • 感谢您提到静态位,但我的意思是:如果 badbar 被声明为静态,即使没有实例化它也不会编译。它背后有规则还是只是 gcc 和 clang 做他们的事情?
    • @swang 将其声明为静态只会触发clang errornot a gcc one。它属于我描述的第一种情况in my answer
    • @MarcoA。你是对的 gcc-4.9 在静态情况下不会发出错误,但某些版本的 gcc 会,例如 4.2、4.4,我不确定 100%,因为我目前没有,但我认为4.8也一样。现在知道名称查找和重载解析之间的区别,根据大家一直在参考的文档,我认为静态声明不应该改变诊断顺序,是否拒绝 cod 可能取决于实现。
    • @swang 编译器没有义务不报告未实例化模板类型中的语义错误。我已编辑我的答案以支持此声明。
    【解决方案5】:

    一开始如果你实例化Devired并尝试调用badbar会出现编译错误:

    // ...
    int main()                                                                      
    {                                                                               
        Derived<int> d;
        d.badbar();
        return 0;                                                                   
    }
    

    生产

    error: too few arguments to function call, single argument 'i' was not specified
    void badbar() { Base::badfoo(); }  // compiles ok
                    ~~~~~~~~~~~~ ^
    

    编译器不编译代码,即没有实例化。

    【讨论】:

    • 你误解了我的问题,我故意不想实例化一个派生对象
    • 好的,我刚刚说过,您没有在 badbar 中收到编译错误,因为您没有实例化 Devired
    • 我已经知道了,这不是我要问的,其他人已经解释过了
    • TBH 我不明白定义一个没有实例化的模板(实际上没用)以及调查编译器如何处理它的意义何在。我猜是纯粹的学术兴趣。
    最近更新 更多