【问题标题】:How to initialize static members in the header如何在标头中初始化静态成员
【发布时间】:2013-09-22 13:02:52
【问题描述】:

Given 是一个具有静态成员的类。

class BaseClass
{
public:
    static std::string bstring;
};

String 显然必须在类之外默认初始化

std::string BaseClass::bstring {"."};

如果我将上述行与类一起包含在标题中,则会收到symbol multiply defined 错误。它必须在单独的cpp 文件中定义,即使是include guardspragma once

没有办法在header中定义吗?

【问题讨论】:

  • 标头不用于初始化。它们用于提供接口声明。
  • @Elazar 如果我必须提供多个定义文件来初始化多个类中的单个成员,则会适得其反,如果我为多个标题提供单个定义文件,则违反直觉。在标头中对其进行初始化将是最舒适的解决方案。
  • 但就是这样。

标签: c++ class static-members


【解决方案1】:

您不能多次定义static 成员变量。如果将变量定义放入标题中,它将在包含标题的每个翻译单元中定义。由于包含保护只影响一个翻译单元的编译,它们也无济于事。

但是,您可以定义static成员函数!现在,乍一看,这似乎没有帮助,当然,该函数可以具有本地 static 变量,并且返回对其中一个变量的引用几乎就像 static 成员变量:

static std::string& bstring() { static std::string rc{"."}; return rc; }

本地static 变量将在第一次调用此函数时被初始化。也就是说,构造会延迟到第一次访问该函数。当然,如果你使用这个函数来初始化其他全局对象,它也可以确保对象被及时构造。如果您使用多个线程,这可能看起来像是潜在的数据竞争,但事实并非如此(除非您使用 C++03):函数本地 static 变量的初始化是线程安全的。

【讨论】:

  • 自 C++11 以来,这并不完全正确。 [class.static.data] §§3 允许为constexpr(强制)和const(可选,仅当文字类型)成员提供初始化程序(brace-or-equal-initializer)。 std::string 不符合条件。
  • 关于“您不能多次定义静态成员变量”,我可以。 OP是否可以是另一回事。 ;-) ODR 对类模板中的静态变量有豁免,因此这是一种方法。
  • 自发布此答案以来,我们已经收到了内联对象提案,我认为它已被 C++17 接受。
【解决方案2】:

在 C++17 中,您可以使用内联变量,甚至可以在外部类中使用。

内联说明符在用于具有静态存储持续时间的变量(静态类成员或命名空间范围变量)的 decl-specifier-seq 时,将变量声明为内联变量。

声明为 constexpr 的静态成员变量(但不是命名空间范围的变量)隐式是内联变量。⁽¹⁾

例如:

class Someclass {
public:
    inline static int someVar = 1;
};

或者,

namespace SomeNamespace {
    inline static int someVar = 1;
}

⁽¹⁾https://en.cppreference.com/w/cpp/language/inline

【讨论】:

  • 您能否详细说明声明变量内联的后果?它会被完全初始化一次(像所有全局初始化数据一样)吗?我假设该对象只有一个实例,但这如何跨翻译单元工作(内联有时可以防止在实现更改时用函数定义替换 DLL,IIRC)。
【解决方案3】:

关于

难道没有办法在header中定义【静态数据成员】吗?

是的。

template< class Dummy >
struct BaseClass_statics
{
    static std::string bstring;
};

template< class Dummy >
std::string BaseClass_statics<Dummy>::bstring = ".";

class BaseClass
    : public BaseClass_statics<void>
{};

正如 Dietmar 建议的那样,另一种方法是使用函数。本质上,这是一个 Meyers 的单身人士(谷歌搜索)。

编辑:另外,由于发布了这个答案,我们已经得到了内联对象提案,我认为它被 C++17 接受。

无论如何,三思而后行这里的设计。全局变量是 Evil™。这本质上是一个全球性的。

【讨论】:

  • 这可以扩展为以某种方式将 bstring 的内容定义为模板参数吗?即类似:class BaseClass : public BaseClass_statics&lt;void,"."&gt;
  • @RickDeckard:我现在看不到任何方法。使用 g++ 4.9.2 进行检查,它需要 constexpr 数组 char 或具有外部链接的数组。我认为具有内部链接的前者将/可能意味着不同翻译单元中模板参数的值本质上不同,而后者与单一定义规则相冲突。定义一个函数来产生值也可以定义一个函数来产生 std::string 值,即 Meyers 的单例方法。然而,我记得曾经为此编写了一个基于宏的方案。但不确定有多普遍。
  • 谢谢!如果您能挖掘该宏,我将不胜感激。我想出的一切都需要 DerivedClass 在标头和 cpp 文件中执行某些操作(宏/模板/等)。这个提议看起来很有趣open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4121.pdf,但不幸的是,我现在需要一些适用于任何 C++11 编译器的东西。
  • 匿名投反对票,请解释你的投反对票。这是错误的,也是错误的。
  • 全局变量通常是邪恶的,但全局常量是可以的。
【解决方案4】:

使用 C++11 中的声明保留静态值的定义 可以使用嵌套的静态结构。在这种情况下,静态成员 是一个结构,必须在 .cpp 文件中定义,但值 在标题中。

class BaseClass
{
public:
  static struct _Static {
     std::string bstring {"."};
  } global;
};

初始化整个静态结构,而不是初始化单个成员:

BaseClass::_Static BaseClass::global;

通过

访问这些值
BaseClass::global.bstring;

请注意,此解决方案仍然存在顺序问题 静态变量的初始化。当使用静态值时 初始化另一个静态变量,第一个可能没有初始化, 还没有。

// file.h
class File {
public:
  static struct _Extensions {
    const std::string h{ ".h" };
    const std::string hpp{ ".hpp" };
    const std::string c{ ".c" };
    const std::string cpp{ ".cpp" };
  } extension;
};

// file.cpp
File::_Extensions File::extension;

// module.cpp
static std::set<std::string> headers{ File::extension.h, File::extension.hpp };

在这种情况下,静态变量 headers 将包含 { "" } 或 { ".h", ".hpp" },具体取决于链接器创建的初始化顺序。

【讨论】:

    【解决方案5】:

    §3.2.6 和当前 c++ 17 草案 (n4296) 中的以下段落定义了不同翻译单元中可以存在多个定义时的规则:

    类类型(第 9 条)、枚举类型(7.2)、内联函数可以有多个定义 外部链接(7.1.2),类模板(第 14 条),非静态函数模板(14.5.6),静态数据成员 类模板 (14.5.1.3) 的成员函数、类模板的成员函数 (14.5.1.1) 或模板特化 在程序中未指定某些模板参数(14.7、14.5.5),前提是每个定义 出现在不同的翻译单元中,并且只要定义满足以下要求。给定 这样一个名为 D 的实体在多个翻译单元中定义,然后 [...]

    显然,类类型的静态数据成员的定义不会出现在多个翻译单元中。因此,按照标准是不允许的

    来自 Cheers and hth 的建议答案。 - Alf 和 Dietmar 更像是一种“黑客”,利用了

    的定义

    类模板的静态数据成员 (14.5.1.3)

    具有外部链接的内联函数 (7.1.2)

    允许在多个 TU 中使用(仅供参考:在类定义中定义的静态函数具有外部链接并隐式定义为内联)。

    【讨论】:

      【解决方案6】:

      不,它不能在标头中完成 - 至少如果标头在您的源文件中包含不止一次,情况似乎就是这样,否则您不会收到这样的错误。只需将其粘贴在其中一个 .cpp 文件中即可。

      【讨论】:

      • 但这不就是 include 守卫或 pragma once 所做的,确保它在编译过程中只包含一次吗?
      • 包含守卫将防止它被多次包含在 .cpp 中(或者,技术上是“翻译单元”,但除非你做奇怪的事情,否则源文件在预处理后是一个翻译单元)。
      • 是的,除了为“接口”类创建一个 cpp 文件很麻烦,否则只需要一个头文件,甚至不需要添加到项目中,因为它不需要编译。一个额外的 cpp 就可以了。
      • 好吧,如果文件中只有这些,那么您也许可以将它添加到另一个已经存在的文件中。
      【解决方案7】:

      更新:我在下面的回答解释了为什么不能按照问题建议的方式完成。至少有两个答案可以绕过这一点;他们可能会也可能不会解决问题。


      bstring 静态成员必须链接到特定的内存地址。为此,它必须出现在单个目标文件中,因此它必须出现在单个 cpp 文件中。除非您使用#ifdef 来确保发生这种情况,否则您无法在头文件中完成您想要的操作,因为您的头文件可能包含在多个cpp 文件中。

      【讨论】:

      • Re“因此”,这里有两个反例(每个作为自己的答案)。
      • @Cheersandhth.-Alf 从技术上讲,我不确定这两者是否是反例。就您的回答而言,您将静态成员移动到继承的类模板中;我怀疑这在语义上是否等效。无论如何,我更新了我的答案,这是在问题发生几分钟后写的,旨在解释为什么不能按照建议完成。干杯...
      • 嗯,怀疑是件好事,托马斯无疑会同意这一点。但无论如何,请注意“要发生这种情况,它必须出现在单个目标文件中”这两个答案都被否定了。 C++支持inline数据的机制,只是没有语法直接表达(可惜)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-04-01
      • 1970-01-01
      • 1970-01-01
      • 2011-09-19
      • 2011-04-19
      • 1970-01-01
      相关资源
      最近更新 更多