能否实现仅包含头文件的非模板类?

3
据我所知,如果要在头文件中实现非模板且非内联的内容,而又不违反一次定义规则,唯一的方法就是在匿名命名空间中实现。但我不确定这样类方法实现中的静态变量会发生什么情况:
// MyHeader.h
// ... pragma once, defines, etc. ...

// (anonymous namespace removed due to suggestions below)
// namespace
// {
    class A // a simplified single-threaded kind of Singleton
    { 
        // ... some private not static data ...
    public:
        static A& instance() 
        {
            static A instance;   // is it guaranteed to be module-global?
            return instance;
        }

        void doNothing();
    }

    // inline added as suggested in answers below
    // actually, I disabled inline in compile settings,
    // checked that inlining was not happened using disassembler, 
    // but `inline` keyword is needed and it matters. Any suggestions, why?
    inline void A::doNothing()
    {
        // it's a very long method, so long that I don't want to 
        // implement it as inline within class body.

        // but not so long to consider refactoring it to smaller functions :)
    }        
//}

// File executable_part1.cpp
// ... includes, etc. ....
A::instance().doNothing();

// File executable_part2.cpp
// ... includes, etc. ....
A::instance().doNothing();

主要问题包括:
  • 是否保证instance是模块全局的?
  • 这是可移植的代码还是行为是编译器实现定义的?

我在Windows上的MSVS 2012上尝试过这个代码。我已经将此头文件包含在3个模块的2个.cpp文件中:一个可执行文件和2个dll,由可执行文件加载。总共6次。

没有名称空间:构造函数被调用了3次,每个“OS级”模块一次。 有名称空间:构造函数被调用了6次,每个cpp文件一次。

更新: 如下所指出,C++'03标准的第7.1.2章解决了我的问题。这里重要的是内联。


1
你的第一个断言是不正确的。请考虑使用 inline 关键字。 - juanchopanza
2
不,只有在类内定义的情况下才可以。重点是你可以在头文件中实现一些非模板内容而无需使用匿名命名空间。 - juanchopanza
1
所以,请查看 inline 关键字。 - juanchopanza
juanchopanza,有没有证据表明如果我在示例中的两个函数中都添加 inline,那么 instance 将在包括我的头文件在内的所有文件中共享? - Victor Istomin
这里有关于 inline 到底是什么意思的好回答吗?如果有的话?一旦我们弄清楚它实际上并没有导致或允许内联,那么它的作用是什么?它允许多个定义(即链接器不会抱怨)? {当然,定义必须完全相同}。所有的模板方法都默认为 inline 吗? - Aaron McDaid
显示剩余8条评论
2个回答

3

不应该使用未命名的名称空间,因为每个翻译单元都会拥有其自己的A结构。

只需执行以下操作:

class A
{ 
    // ... some private not static data ...
public:
    static A& instance() 
    {
        static A instance;
        return instance;
    }

    void doNothing();
};

inline void A::doNothing()
{
    // it's a very long method, so long that I don't want to 
    // implement it as inline within class body.

    // but not so long to consider refactoring it to smaller functions :)
}
  • 单例在exe/dll之间不共享是正常的。
  • 使用未命名的命名空间,对于每个TU都有单例,而没有它,则单例在整个exe(以及整个Dll)中共享。

对于您的配置:1个exe和2个cpp,以及两个具有2个cpp的Dll:

您的实验是正确的:有6个未命名的命名空间,没有它则有3个。


没错!即使编译器设置禁用了内联,这也能正常工作,因此即使编译器忽略了 inline,它也可以正常运行。你能解释一下为什么吗?没有 inline 就会产生错误。 - Victor Istomin
谢谢。关于不在.exe和.dll之间共享的问题,我同意,这是正常的,并且是按类设计的。对我来说唯一未解决的问题是,即使禁用了内联,为什么此代码使用inline也能工作?这是编译器相关的代码吗?另外,我检查了反汇编中实际上没有发生内联。 - Victor Istomin
1
编译器是否真正内联代码是独立的事实。并且它不是特定于编译器的。 - Jarod42
1
明白了,谢谢。刚刚找到了证据:在“03标准(ISO/IEC 14882:2003)”的第7.1.4章节中写道:“内联函数应该在每个使用它的翻译单元中定义,并且在每种情况下都应该有完全相同的定义(3.2)。...(省略部分)... 具有外部链接的内联函数在所有翻译单元中具有相同的地址。extern inline函数中的静态局部变量始终引用同一对象。在extern inline函数中的字符串文字是不同翻译单元中的同一对象。” - Victor Istomin

-1

我真的不太理解问题(也许我错过了什么),所以保留你需要的部分。

据我理解,静态方法和数据属于类本身,而不是类的实例。 因此,静态数据只存在一次,并且无论从哪个对象访问它们,它们都是相同的。 静态方法就像普通的外部函数,它们没有可用的this指针。 属于类的命名空间,因此每个类都可以实现具有相同名称的静态函数,并且不会发生名称冲突,因为它们的名称不相等,完全限定名称为:“classOfAppartenence :: nameOfFunction”,对于每个类,完全限定名称都是不同的。 基本上,如果您不为类实现任何对象,则该类将作为命名空间工作。


"我真的没有很好地理解这个问题",这需要一个评论,而不是一个答案! - πάντα ῥεῖ
据我理解,静态变量在语义上只是在类的命名空间中定义的全局变量。因此,如果在不同的文件中全局变量被视为相同,则在类的命名空间中的“全局变量”也会发生相同的情况。反之,如果由于某种原因全局变量不能被视为相同,则“类的全局变量”也是如此。 - George Kourtis
我没有完全理解你的评论。基本上,我同意,但我不确定包含静态变量的内联函数是否会在不同的翻译单元中引用相同的静态变量。此外,我不确定声明为“内联”的“非内联”函数是否会导致链接器错误。 - Victor Istomin
据我理解,内联函数唯一的区别在于它们没有任何名称暴露给链接器供其使用。因此,正常函数发生的事情也会发生在内联函数中,但您可能在两个不同的编译单元中拥有两个具有相同名称的不同内联函数,并且由于该名称不被公开,因此它们不会冲突。 - George Kourtis
因此,内联函数所处理的内容与普通函数所处理的内容完全相同。当普通函数访问类的静态变量时,只能看到其编译单元中的“本地”类,无法看到其他编译单元中的“其他”类。这背后的逻辑是,类、类型和模板仅在编译期间存在,因此当编译结束时,它们就不存在了。相反,对象和函数是编译留下的“数据”,因此可以从任何编译单元访问它们。请注意,我的话只是我个人的理解,请务必核实! - George Kourtis
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接