C++中未命名类的静态数据成员

7

5
如果班级没有名称,您会如何称呼成员? - molbdnilo
@molbdnilo 你可以在类内部引用它,但问题是,正如答案所描述的那样,实际的定义需要从外部完成。 - skyking
1
@molbdnilo struct { int x; } a_variable; &decltype(a_variable)::x 可以工作,所以没有理由 decltype(a_variable)::static_data_member; 不能工作。 - Jean-Michaël Celerier
3个回答

13

如果static成员数据被进行了ODR使用,则必须在类/结构体之外定义。

struct Foo
{
    static int d;
};

int Foo::d = 0;
如果类/结构体没有命名,则无法在类外定义成员。
int ::d = 0;

不能用于定义未命名类的静态成员。

C++17更新

如果您能使用C++17或更高版本,则可以使用

static inline int d = 10;

这将允许在匿名的/结构体中定义一个静态成员变量。

以下示例代码演示了一个静态成员变量不需要在类定义外部定义:

#include <iostream>
struct foo
{
    static inline int d = 10;
};

int main()
{
   auto ptr = &foo::d;
   std::cout << *ptr << std::endl;
}

构建命令:

g++ -std=c++17 -Wall    socc.cc   -o socc

程序运行的输出结果:

10

感谢 @Jean-Michaël Celerier 提供更新建议。


1
在C++17中,我们有“static inline”,它消除了此限制:结构体Foo { static inline int d; }; - Jean-Michaël Celerier
@Jean-MichaëlCelerier,好的,知道了。你知道这是否足够用于ODR中使用的“static”成员变量吗? - R Sahu
2
确保:https://gcc.godbolt.org/z/h7xMrsdcd;这是链接器的工作,确保最终程序中只有一个`d`实例。 - Jean-Michaël Celerier
@Jean-MichaëlCelerier,感谢您的更新。 - R Sahu

1

你确定标准实际上禁止这样做吗?

正如所提到的,问题在于需要对静态成员进行实际定义。语言没有提供定义它的方法。在结构体内部或通过其实例引用它没有其他问题。

然而,例如GCC将接受以下内容:

static struct {
    static int j;
} a;

int main() {
    return a.j; // Here we actually refers to the static variable
}

但是它无法链接,因为a.j指向一个未定义的符号(._0::j),但有一种方法可以解决这个问题。通过在汇编中定义它或使用编译器扩展,您可以解决此问题。例如,添加以下行:
int j asm("_ZN3._01jE") = 42;

我会让它起作用。在这种情况下,_ZN3._01jE是静态变量的真实名称,但无论是编译后还是未编译的名称都不能直接用作标准C++中的标识符(但可以通过GCC扩展或汇编器使用)。

你可能已经意识到,这只适用于特定的编译器。其他编译器可能以其他方式混淆名称(甚至执行其他可能使技巧根本不起作用的操作)。

你应该真正质疑为什么要使用这个技巧。如果你可以使用标准方法完成工作,那么最好选择标准方法。例如,你可以使用匿名namespace来降低可见性:

namespace {
    static struct Fubar {
         static int j;
    } a;

    Fubar::a = 0;
}

现在Fubar并不是真正的匿名,但至少它将被限制在翻译单元中。

1
@HiGuy 不,不是那么奇怪 - 你可能会得到一个未定义的符号,因为名称会有所不同。在某些情况下,在生产代码中使用编译器特定的行为是可以接受的(在我看来)。但是我无法给出使用这个特殊技巧的合理理由。 - skyking
@g24l 他并没有暗示需要使用多个编译器/链接器来生成部署文件,而是当您为多个平台编译软件包时,将需要多个编译器。一个编译器可能会忽略的错误,在其他编译器上可能不会被忽略,导致交叉编译问题的复杂网络。做这种“花哨”的事情永远不是“好的” - 除非绝对没有其他方法来解决给定的问题(这是极其不可能的)。 - Zac Howland
@ZacHowland,嗯,你知道,可移植性并不总是必需的。 - g24l
@skyking,这不是可移植性的问题。而是利用特定编译器中的错误,使您的代码难以维护。提供的两个示例(嵌入式和操作系统)完全不准确(如果您曾经在Linux内核上做过任何事情,您会看到为什么要避免这些情况 - 当有人提交利用特定编译器错误解决问题的低级代码时,Linus发了一通牢骚)。 - Zac Howland
@ZacHowland 首先,正如我所指出的,我认为我已经清楚地表明了我不认为有任何情况可以证明这种特殊技巧是合理的。其次,我认为称它们两者都不准确是错误的 - 第二个有什么不准确之处?在我的看法中,将编译器特定扩展称为故障有点过于苛刻了。如果你考虑到源代码已经使用了GCC特定的扩展功能,那么这样做会有点虚伪,因为这是Linus的立场。 - skyking
显示剩余7条评论

1
当C++标准化时,未命名类无法具有静态数据成员,因为没有定义/实例化它们的方法。然而,C++11解决了这个问题,因为它添加了decltype运算符:
struct {
    static int j;
} a;

// Declare the static member of the unnamed struct:
int decltype(a)::j = 42;

int main() {
    return a.j == 42 ? 0 : 1; // Use the static member 
}

因此,在原则上,可能存在具有静态数据成员的未命名类或结构体。但是,C++标准制定者故意不允许这种语法,因为编译器不知道应该为那个decltype(a)::j东西赋予什么名称以进行链接。因此,大多数(全部?)编译器 - 包括正常模式下当前版本的GCC - 拒绝编译此代码。
在模式下,GCC-9和GCC-10接受此代码并成功编译。但是,如果将a的声明移动到包含在不同源文件中的头文件中,则它们仍然在链接阶段失败。
因此,未命名类只能在单个翻译单元内使用。为了避免污染全局命名空间,只需将任何需要保持本地的内容放入匿名命名空间中。所以:
namespace {
    struct whatever {
        static int j;
    } a;
    int whatever::j = 42;
}
int main() {
    return a.j == 42 ? 0 : 1; 
}

编译正常,不会污染全局命名空间,即使名称whatever与另一个头文件中的名称冲突也不会导致问题。

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