未使用的函数能否根据 C++14 实例化具有副作用的变量模板?

Can an unused function instantiate a variable template with side-effects according to C++14?

这是我的代码:

#include <iostream>

class MyBaseClass
{
public:
    static int StaticInt;
};

int MyBaseClass::StaticInt = 0;

template <int N> class MyClassT : public MyBaseClass
{
public:
    MyClassT()
    {
        StaticInt = N;
    };
};

template <int N> static MyClassT<N> AnchorObjT = {};

class UserClass
{
    friend void fn()
    {
        std::cout << "in fn()" << std::endl; //this never runs
        (void)AnchorObjT<123>;
    };  
};

int main()
{
    std::cout << MyBaseClass::StaticInt << std::endl;
    return 0;
}

输出为:

123

...指示 MyClassT() 构造函数被调用,尽管 fn() 从未被调用。

gccclang 上测试 -O0-O3-Os 甚至 -Ofast


问题

根据 C++ 标准,此程序是否具有未定义的行为?

换句话说:如果更高版本的编译器设法检测到 fn() 永远不会被调用,他们可以优化模板实例化以及 运行 构造函数吗?

能否以某种方式使此代码具有确定性,即强制构造函数 运行 - 引用函数名称 fn 或模板参数值 123UserClass?

之外

更新: 版主 t运行 回答了我的问题并建议进一步 t运行cation。详细原版可以查看here.

"If later versions of compilers manage to detect that fn() will never be called [and] they optimize away template instantiation" 那么那些编译器就会被破坏。

C++ 编译器可以自由实现任何优化没有明显效果。在您概述的情况下,至少会有一个可观察到的效果:即静态 class 成员不会被构造和初始化,因此 C++ 编译器无法完全优化它。这不会发生。

编译器可以忽略有关函数调用的所有其他内容,而不是实际编译函数调用本身,但编译器必须做任何它需要做的事情来进行安排,以便初始化静态 class 成员好像调用了函数。

如果编译器可以确定程序中没有其他任何东西实际使用静态class成员,并且完全删除它没有观察到的效果,那么编译器可以删除静态class成员,以及初始化它的函数(因为没有其他东西引用该函数)。

请注意,即使获取函数(或 class 成员)的地址也会产生可观察到的效果,因此即使实际上没有任何东西调用函数,但某些东西会获取函数的地址,它不能就此消失。

P.S。 -- 以上所有假设 C++ 代码中没有未定义的行为。随着未定义的行为进入画面,所有规则都消失了 window。

模板实例化是代码的一个函数,而不是任何一种动态运行时间条件的函数。举个简单的例子:

template <typename T> void bar();

void foo(bool b) {
  if (b) {
    bar<int>();
  } else {
    bar<double>();
  }
}

bar<int>bar<double> 都在此处实例化,即使 foo 从未被调用或即使 foo 仅被 true 调用。

对于变量模板,具体规则为[temp.inst]/6:

Unless a variable template specialization has been explicitly instantiated or explicitly specialized, the variable template specialization is implicitly instantiated when it is referenced in a context that requires a variable definition to exist or if the existence of the definition affects the semantics of the program.

在你的函数中:

friend void fn() 
{ 
    (void)AnchorObjT<123>;
};  

AnchorObjT<123> 在需要定义的上下文中被引用(无论 fn() 是否曾经被调用,甚至在这种情况下,甚至可以调用),因此它被实例化.

但是AnchorObjT<123>是一个全局变量,所以它的实例化意味着我们有一个在main()之前构造的对象——当我们进入main()AnchorObjT<123>的构造函数将是 运行,将 StaticInt 设置为 123。请注意,我们实际上不需要 运行 fn() 来调用此构造函数 - fn() 在这里的作用只是实例化变量模板,其构造函数在其他地方调用。

打印 123 是正确的预期行为。


请注意,虽然该语言要求全局对象 AnchorObjT<123> 存在,但链接器可能仍然是该对象,因为没有对它的引用。假设您的真实程序对这个对象做更多的事情,如果您需要它存在,您可能需要对它做更多的事情以防止链接器删除它(例如 gcc 有 used attribute)。

简短的回答是有效。

长的答案是它有效除非 linker 丢弃你的整个翻译单元 (.obj)。

当您创建 .lib 并 link 它时,可能会发生这种情况。 linker 通常会根据 "do I use symbols that obj exports".

的依赖关系图从库中选择哪个 .obj 到 link

因此,如果您在 cpp 文件中使用此技术,则该 cpp 文件没有在您的可执行文件的其他地方使用的符号(包括通过您的 lib 中的其他 obj 间接使用的符号,这些符号又由可执行文件使用),link用户可能会丢弃您的 obj 文件。

我用 clang 体验过这个。我们在哪里创建自注册工厂,有些在哪里被删除。为了解决这个问题,我们创建了一些宏,这些宏导致了一个微不足道的依赖关系的存在,从而防止 obj 文件被丢弃。

这与其他答案并不矛盾,因为 link 一个库的过程是在决定你的程序中什么是什么不是。