【问题标题】:global const objects in cpp with no runtime initializationc ++中的全局常量对象,无需运行时初始化
【发布时间】:2020-12-19 03:26:00
【问题描述】:

如何在 cpp 中声明一个全局 const 对象,以便其关联的数据完全存储在 .rodata 中,而不是在运行时初始化期间创建并不必要地复制该对象?

例如,如果我创建类型的全局变量

  • const std::string
  • const std::array<const std::string, 4>
  • const std::map<std::string, const std::string>

测试告诉我这些将编译成 .bss,因此需要运行时初始化,尽管它是编译时已知的常量数据......并且它需要知道将它们初始化为什么,因此它也不必要地复制数据,使用额外的内存。

如何在没有任何运行时初始化的情况下获取驻留在 .rodata 中的实际 const 对象?

由于 C++ 标准可能对此不够具体,如果您需要一些编译器特定的功能,g++ 和/或 clang++ 支持的功能将不胜感激。

注意:,如果您的答案是某种提升或某种特定库的东西,请解释该库是如何实现这一点的。我想了解这是如何实现的。


以下注释可以忽略,但包括在内,因为我不断从人们那里得到的最初反应是“不可能,一个 const 字符串或 const 数组不需要运行时初始化或重复数据”。

所以这里有一个例子和一些测试:

test.cpp

#include <iostream>
#include <fstream>
#include <string>
#include <array>
#include <map>

std::string str {"here"};
const std::string cstr {"there"};
std::array<std::string, 3> arr {"eight", "six", "seven"};
const std::array<const std::string, 4> carrc {"five", "nine", "oh", "three"};

const std::map<std::string, const std::string> cmapc = {
    {"a", "apple"},
    {"b", "bananna"},
    {"c", "carrot"},
};

void show_info(const char *name, const void *a, const void *b)
{
    std::cout << name << "\t" << a << " " << b << std::endl;
}

int main(int argc, char **argv) {
#define INFO(x) show_info(#x, &x, x.data())
    INFO(str);
    INFO(cstr);

    INFO(arr);
    INFO(arr[1]);
    INFO(carrc);
    INFO(carrc[1]);

    std::cout << "cmapc" << "\t" << (void *)&cmapc << std::endl;
    INFO(cmapc.at("a"));


    std::ifstream infile("/proc/self/maps");
    std::string line;
    while(std::getline(infile, line)) {
        std::cout << line << std::endl;
    }

    return 0;
}

编译并检查对象的放置位置

$ g++ -std=c++17 -o test test.cpp
$ readelf -W -S test | grep -E "(.rodata|.data|.bss)"
  [16] .rodata           PROGBITS        0000000000005ad0 005ad0 0000e9 00   A  0   0  8
  [24] .data             PROGBITS        0000000000209000 009000 000018 00  WA  0   0  8
  [25] .bss              NOBITS          0000000000209020 009018 000290 00  WA  0   0 32
$ readelf -s test | grep OBJ | grep -E "[^_](str|arr|map)"
    37: 00000000002091e0    32 OBJECT  LOCAL  DEFAULT   25 _ZL4cstr
    38: 0000000000209200   128 OBJECT  LOCAL  DEFAULT   25 _ZL5carrc
    39: 0000000000209280    48 OBJECT  LOCAL  DEFAULT   25 _ZL5cmapc
    88: 0000000000209160    96 OBJECT  GLOBAL DEFAULT   25 _Z3arrB5cxx11
   105: 0000000000209140    32 OBJECT  GLOBAL DEFAULT   25 _Z3strB5cxx11

您也可以只运行程序并查看输出。

或者查看 gdb 中的运行时初始化

$ gdb -q ./test
Reading symbols from ./test...(no debugging symbols found)...done.
(gdb) b _start
Breakpoint 1 at 0x2180
(gdb) r
Starting program: /tmp/test

Breakpoint 1, 0x0000000008002180 in _start ()
(gdb) x/4gx &str
0x8209140 <_Z3strB5cxx11>:      0x0000000000000000      0x0000000000000000
0x8209150 <_Z3strB5cxx11+16>:   0x0000000000000000      0x0000000000000000
(gdb) b main
Breakpoint 2 at 0x800230f
(gdb) c
Continuing.

Breakpoint 2, 0x000000000800230f in main ()
(gdb) x/4gx &str
0x8209140 <_Z3strB5cxx11>:      0x0000000008209150      0x0000000000000004
0x8209150 <_Z3strB5cxx11+16>:   0x0000000065726568      0x0000000000000000

【问题讨论】:

  • std::string 为堆上的实际字符分配内存。我不相信有办法避免对其进行动态初始化。
  • std::map 相同,它如何在编译时创建它的树?
  • 您根据哪个标准编译代码?采用const char *std::string 的构造函数是在C++20 中创建的constexpr。我尚未对其进行测试,但这表明短字符串可能会在编译时构建。
  • @IgorTandetnik 看看我的例子,对于全局 const std::string 它不会在堆上。它在 .bss
  • @Someprogrammerdude 它知道编译时的所有数据,所以至少看起来可行。

标签: c++ memory-management constants


【解决方案1】:

您可以在编译时通过定义支持对象的构造函数来定义对象。注意对象的成员也应该是const

在下面的例子中,我们在编译时创建了一个对象foo。我们可以使用static_assert 函数来验证这一点。另一方面, foo2 是在运行时创建的。不幸的是,没有灵丹妙药可以解决您的问题。使用constexpr 并不能保证您总是会在编译时初始化,因为这只会在可能的情况下发生,如果不发生,编译器不会抱怨。

struct Foo {
  const int elem1;
  const char elem2;
  constexpr Foo(int a, char b);
};

constexpr Foo::Foo(int a, char b) :
  elem1(a), elem2(b)
  {
  }

constexpr Foo foo(1, 'a');

static_assert(foo.elem1 == 1);
static_assert(foo.elem2 == 'a');

int main()
{
  Foo foo2(2, 'b');
}

【讨论】:

  • 谢谢,我用clang++-10试过了,它成功地将foo放入.rodata
  • 我无法弄清楚如何将 elem2 更改为 std::string 作为示例,但是按照stackoverflow.com/questions/27123306/… 我可以将其设置为 std::string_view 并且全部进入 .rodata 所以解决字符串。是否有某种 constexpr 兼容的地图?
  • 我不知道是否有办法在标准 C++ 中实现这一点。我认为这是不可能的,因为地图中的分配器对象动态处理存储。但是,我发现了这个 C++ 头文件 (github.com/mapbox/eternal),它实现了编译时映射和哈希映射。不过我自己没试过。
  • 虽然标准库可能不支持它,但至少原理上很清楚这是如何实现的。现在我知道要搜索什么,有很多示例(直到现在我认为 constexpr 只是用于编译时表达式,但现在它也可以用于帮助编译时布局)。这是一个具有 constexpr 映射的对象 github.com/serge-sans-paille/frozen
猜你喜欢
  • 1970-01-01
  • 2017-08-02
  • 1970-01-01
  • 2021-05-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-11-20
相关资源
最近更新 更多