静态常量在包含多个翻译单元时的开销?

7
在头文件中,全局常量可以在单行中声明和(预)定义。
// constants.h
namespace Constant{
    static const unsigned int framerate = 60;
    static const char * const windowName = "Test";
    static const unsigned char * const cursorBitmap = { lots of data };
}

我喜欢这种格式,因为它让我把常量放在一个地方,避免了需要在一个文件中声明常量并在另一个文件中定义常量的情况,有助于提高可读性。但是,当任何翻译单元包含“constants.h”时,它会在每个单元中扩展这些定义。
我的问题是,如果我将“constants.h”包含到许多翻译单元中,并且例如“cursorBitmap”和其他数组常量非常大,那么这会导致显着的开销吗?如果我将其包含到100个单元中,那么我的程序是否会包含100个字符串和数组文字的副本?还是只会复制指针和值?
如果有开销,是否有一种方法可以避免它,而无需分离声明和定义?
(此外,我猜测这种用法中的“static”是多余的,但我仍然喜欢把它放在那里)

2
另外值得注意的是,C++17草案中引入了inline变量。 - Simple
这并不会避免在另一个文件中定义这些变量。这些是声明,在许多情况下你不必提供定义,但在某些情况下你需要。例如,如果你将任何这些常量传递给一个按引用传递的函数,你将需要一个定义。 - Pete Becker
3个回答

3
无论字符串字面量是否在各个翻译单元中被复制,这都是一个实现质量问题。
直接声明的对象将在包含此头文件的每个翻译单元中被复制。虽然不多,但在某些常量地址未被直接或间接使用的翻译单元中,它可能会被优化掉。
如果您想确保每个常量只有一个副本,甚至没有副本,则可以使用类模板,例如:constants.h
#pragma once

template< class Dummy >
struct Constant_{
    static const unsigned int framerate;
    static const char * const windowName;
    static const unsigned char * const cursorBitmap;
};

template< class Dummy >
const unsigned int Constant_<Dummy>::framerate = 60;

template< class Dummy >
const char * const Constant_<Dummy::windowName = "Test";

template< class Dummy >
const unsigned char * const Constant_<Dummy>::cursorBitmap = ...;

using Constant = Constant_<void>;

但是在我看来,这比它所值的更需要努力。

类似的替代方法是使用内联函数,每个常量都有一个。


TU与最终链接的二进制文件有什么关系?如果我将其包含在100个单元中,我的程序是否会包含每个字符串和数组文字的100个副本?非静态情况又如何? - xaxxon
@xaxxon:库和程序是两种不同的东西。这个问题是关于程序的。普通的库是一组(翻译后的)翻译单元,可以单独或批量使用。很难想到一种避免重复的方法,但这仍然是实现质量的问题。 - Cheers and hth. - Alf
@xaxxon:关于“非静态变量”,您可能指的是外部链接。上面的模板技术生成与仅包含头文件的模块兼容的外部链接常量。另一种选择是在实现文件中定义常量,但OP表示她希望避免这样做,“避免需要在一个文件中声明常量并在另一个文件中定义它”。 - Cheers and hth. - Alf

1
如果我将它包含在100个单元中,我的程序会包含每个字符串和数组文字的100个副本吗?还是只会复制指针和值?
标准没有承诺字符串文字的合并,因此取决于实现。在GNU/Linux上使用GCC 5.1.1进行简单测试表明,在未优化的构建中,字符串文字不会被合并,但在使用-O-Os时会合并。但这仅仅是合并实际的char数组。然而,只要编译器没有优化指针或数值常量的存储(如果它们是翻译单元局部的POD类型,并且未ODR使用,则它们是显然的消除候选项),编译器可能无法轻松地将它们合并。标准要求它们是不同的对象,因此必须具有不同的地址。即使您获取了它们的地址,根据as-if规则,仍然可以删除它们,但这通常需要全局程序优化,也就是链接时优化,包括库,实现可能不支持、仅以有限的方式支持和/或仅取决于编译器和链接器设置。换句话说,在正确的情况下它可能会发生,但更有可能不会发生。
我的测试表明,即使使用了-Os -flto(为大小进行优化并启用链接时优化),GCC 5.1.1也不会合并由const引用公开的static const unsigned int对象。如果有任何现代实现执行这种困难和晦涩的优化,我会感到惊讶。
(我猜在这种用法中“static”是多余的,但我仍然喜欢把它放在那里)
如果您有多个翻译单元,则它不是多余的,因为否则您将违反一个定义规则(ODR)。然而,值得一提的是,自C++98以来,命名空间作用域中的static已被认为是语法上过时的(考虑使用匿名命名空间)。
(回答Cheers和hth.-Alf)
如果您想确保每个常量仅有一个副本,甚至没有副本,那么可以使用类模板,如下所示:

没有这样的运气。标准中并没有保证模板使用了多少空间。所有模板保证的是只使用了潜在的多个副本中的一个——或者根据 as-if 规则看起来使用了一个。事实上,情况比这还糟糕,因为至少在我的系统上,即使使用了 -Os -flto,GCC 5.1.1 实际上也不会删除冗余的 static const unsigned int。这意味着,在两个翻译单元中,unsigned int 的初始化值可以在两个不同的位置找到,尽管只有其中一个被使用(所有指针和引用都只引用此位置)。


0

首先,自C++98以来,在命名空间中使用static已被弃用:

D.2 static关键字
在命名空间作用域中声明对象时,使用static关键字已被弃用(参见3.3.5)

其次,在C++中,const本身就意味着内部链接。

第三,确切的答案取决于您使用的编译器和选项。可以通过编译器/链接器消除重复项,特别是如果可以使用LTO(链接时间优化)。


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