C++中的等价于指定初始化器是什么?

37

最近我一直在从事嵌入式设备的工作,在其中我们有一些结构体和联合体需要在编译时初始化,以便我们可以将某些不需要修改的内容保存在flash或ROM中,并在一定程度上节省一些flash或SRAM空间,虽然这会带来一点性能成本。当前代码编译为有效的C99代码,但如果没有进行调整,它也可以编译为C++代码,因此支持以这种方式进行编译将是很好的。其中一个主要的问题是我们使用了C99指定初始值设定项,但它们在C++子集中无法运行。 我不是很精通C ++,因此想知道是否有简单的方法可以在兼容C ++的C中实现此目标,或者在C ++中实现此目标仍然允许在编译时初始化结构体和联合体,以便这些结构体和联合体不需要在程序启动后在SRAM中进行初始化。

另外值得注意的一点是,指定初始值设定项的关键原因是要初始化联合体中的非第一个成员。另外,坚持使用标准的C++或ANSI C有助于与其他编译器保持兼容(我知道GNU扩展提供了类似于C99指定初始值设定项的功能)。


1
请注意,指定的初始化器现在可以在g++中使用。我有4.8.1版本,并且我可以从枚举中使用初始化器,它的效果正如预期。 - Alexis Wilke
6个回答

23

我不确定你是否可以在C++中实现这个功能。对于需要使用指定初始化器进行初始化的内容,您可以将其分别放在一个以C99编译的.c文件中,例如:

// In common header file
typedef union my_union
{
    int i;
    float f;
} my_union;

extern const my_union g_var;

// In file compiled as C99
const my_union g_var = { .f = 3.14159f };

// Now any file that #include's the header can access g_var, and it will be
// properly initialized at load time

2
我已经考虑过这个问题,最终可能会采用这种方法。不幸的是,我们想要移植到的一个平台使用C++库来提供外设支持。他们提供在线编译器访问权限,但为了简单起见,他们将其限制为仅C++模式。我尝试过获取预编译库并使用本地GCC工具链,但在与GCC和Newlib链接时,无法解析一些符号(他们使用Keil/ARM的RealView)。我可以在本地预编译所有C99代码,然后在线链接。我只是想保持简单 :-) - James Snyder
这个方法可能可行,但是如果你在联合体/结构体的成员中使用非标准类型,很快就会变得混乱不堪。 - Zimano

18

在Shing Yip的回答基础上,并且有了三年的时间,C++11现在可以保证编译时初始化:

union Bar
{
    constexpr Bar(int a) : a_(a) {}
    constexpr Bar(float b) : b_(b) {}
    int a_;
    float b_;
};

extern constexpr Bar bar1(1);
extern constexpr Bar bar2(1.234f);

汇编语言:

    .globl  _bar1                   ## @bar1
    .p2align    2
_bar1:
    .long   1                       ## 0x1

    .globl  _bar2                   ## @bar2
    .p2align    2
_bar2:
    .long   1067316150              ## float 1.23399997

2

我既是回答者也是提问者。我知道这个帖子已经过期了,但今晚我正好在研究这个问题。

我查了一些资料,最接近我想要的(与你所需相似...我一直在处理图片,没有使用c++的必要,但我很好奇它如何实现)是第一个代码示例:

#include <iostream>

using namespace std;

extern "C" 
{
    typedef struct stuff
    {
        int x;
        double y;
    } things;
}

int main()
{
    things jmcd = { jmcd.x = 12, jmcd.y = 10.1234 };
    cout << jmcd.x << " " << jmcd.y << endl;
    return 0;
}

这个看起来与C99样式的指定初始化器非常相似,但有一个我稍后会提到的警告。(如果你想让结构体被编译,请使用#ifdef __cplusplus包装它。) 我看过的第二个代码版本是这样的:

#include <iostream>

using namespace std;

extern "C" 
{
    typedef struct stuff
    {
        int x;
        double y;
    } things;
}


int main()
{
    things jmcd;
    jmcd.x = 12;
    jmcd.y = 10.1234;
    cout << jmcd.x << " " << jmcd.y << endl;
    return 0;
}

基本上,从反汇编的结果来看,第一个例子实际上更慢。我已经查看了汇编输出,但是,我可能有点生疏。也许有人可以给我一些见解。第一个cpp编译的汇编输出如下:

main:
.LFB957:
    .cfi_startproc
    .cfi_personality 0x0,__gxx_personality_v0
    pushl   %ebp
    .cfi_def_cfa_offset 8
    movl    %esp, %ebp
    .cfi_offset 5, -8
    .cfi_def_cfa_register 5
    subl    $24, %esp
    movl    $0, 12(%esp)
    movl    $0, 16(%esp)
    movl    $0, 20(%esp)
    movl    $12, 12(%esp)
    movl    12(%esp), %eax
    movl    %eax, 12(%esp)
    fldl    .LC0
    fstpl   16(%esp)
    fldl    16(%esp)
    fstpl   16(%esp)
    movl    12(%esp), %eax
    movl    %eax, 4(%esp)
    fildl   4(%esp)
    fldl    16(%esp)
    faddp   %st, %st(1)
    fnstcw  2(%esp)
    movzwl  2(%esp), %eax
    movb    $12, %ah
    movw    %ax, (%esp)
    fldcw   (%esp)
    fistpl  4(%esp)
    fldcw   2(%esp)
    movl    4(%esp), %eax
    leave
    ret
    .cfi_endproc

第二个示例看起来像这样:
main:
.LFB957:
    .cfi_startproc
    .cfi_personality 0x0,__gxx_personality_v0
    pushl   %ebp
    .cfi_def_cfa_offset 8
    movl    %esp, %ebp
    .cfi_offset 5, -8
    .cfi_def_cfa_register 5
    subl    $24, %esp
    movl    $12, 12(%esp)
    fldl    .LC0
    fstpl   16(%esp)
    movl    12(%esp), %eax
    movl    %eax, 4(%esp)
    fildl   4(%esp)
    fldl    16(%esp)
    faddp   %st, %st(1)
    fnstcw  2(%esp)
    movzwl  2(%esp), %eax
    movb    $12, %ah
    movw    %ax, (%esp)
    fldcw   (%esp)
    fistpl  4(%esp)
    fldcw   2(%esp)
    movl    4(%esp), %eax
    leave
    ret
    .cfi_endproc

这两个示例都是使用g++ -O0 -S main.cpp命令生成的。显然,直观上效率较低的示例在指令数量方面生成了更有效的操作码。另一方面,有少数情况下,我可以想象这几条指令非常关键。(另一方面,我真的很难理解不是人类编写的汇编代码,所以也许我错过了什么……)我认为这提供了一个解决方案,虽然有点晚,但回答了詹姆斯的问题。接下来我应该测试的是同样的初始化在C99中是否被允许;如果可以,我认为它完全解决了詹姆斯的问题。

免责声明:我不知道这是否适用于除g++之外的其他编译器。


对我来说没问题 - 不管怎么样也不是快速通道的一部分。比使用另一个C文件更好的解决方案!点赞! - Sam
5
如果在第一个解决方案中交换jmcd.x和jmcd.y,再次迟到肯定行不通。这是因为这不是一个特殊的结构,而只是更多表达式的常规初始化。所以jmcd.x = 12会被执行,然后将此表达式(12)的结果值分配给结构体的第一个字段(x)。y也是一样。如果你交换它们,两个字段都将变成12。 - Asaf
C++中的指定初始化器支持是GNU扩展,不属于C++11的一部分。即使使用GCC扩展,指定初始化仅允许用于POD类型。 - chys

2
#ifdef __cplusplus
struct Foo
{
    Foo(int a, int b) : a(a), b(b) {}
    int a;
    int b;
};

union Bar
{
    Bar(int a) : a(a) {}
    Bar(float b) : b(b) {}
    int a;
    float b;
};

static Foo foo(1,2);
static Bar bar1(1);
static Bar bar2(1.234f);
#else 
 /* C99 stuff */
#endif // __cplusplus

在C++中,联合体(union)也可以有构造函数。也许这正是您想要的?

初始化是在运行时还是编译时完成的?这应该是关键问题。 - James Snyder
我需要查阅圣经标准以确保,但就我个人的理解,我认为所有全局静态变量都会在可执行映像的数据段中初始化和存储。你最好尝试使用你的编译器来验证一下它的行为。 - Shing Yip
1
不会,空间将在可执行映像中分配,初始化为零,并在运行时调用构造函数。 但是,为了增加乐趣,嵌入式系统在这一点上可能有些不一致-理论上,链接器会收集静态构造函数调用列表,然后libgcc在进程引导期间调用它们,但根据您的平台,它可能根本不会发生,或者只有在选择正确的构建选项时才会发生。 - Tom

0

下面的代码可以使用g++编译而没有问题:

#include <iostream>

struct foo
{
  int a;
  int b;
  int c;
};

union bar
{
  int a;
  float b;
  long c;
};

static foo s_foo1 = {1,2,3};
static foo s_foo2 = {1,2};
static bar s_bar1 = {42L};
static bar s_bar2 = {1078523331}; // 3.14 in float


int main(int, char**)
{
  std::cout << s_foo1.a << ", " <<
               s_foo1.b << ", " <<
               s_foo1.c << std::endl;

  std::cout << s_foo2.a << ", " <<
               s_foo2.b << ", " <<
               s_foo2.c << std::endl;

  std::cout << s_bar1.a << ", " <<
               s_bar1.b << ", " <<
               s_bar1.c << std::endl;

  std::cout << s_bar2.a << ", " <<
               s_bar2.b << ", " <<
               s_bar2.c << std::endl;

  return 0;
}

这是结果:

$ g++ -o ./test ./test.cpp
$ ./test
1, 2, 3
1, 2, 0
42, 5.88545e-44, 42
1078523331, 3.14, 1078523331

C++的初始化器唯一需要注意的是,你需要初始化结构体的所有元素,否则其余部分将被初始化为零。你不能挑选和选择。但对于您的用例来说,这仍然应该是可以接受的。

另一个需要注意的重点:指定初始化程序使用的一个关键原因是初始化不是联合体的第一个成员。

为此,您需要使用示例中显示的“解决方法”,通过提供等效的int值来设置“float”成员。这有点像黑客行为,但如果它解决了您的问题,那就好了。


这只是因为42L被隐式转换为整数。如果你想将浮点成员初始化为3.5,你在C++中无法做到这一点。 - Adam Rosenfield
你不能初始化联合类型的多个成员,毕竟它是一个联合体;-) 但是如果你想初始化“float”部分,你需要使用整数等效值(可能是十六进制数)来初始化。 - lothar
7
是的,但C99标准支持指定初始化器,让你能够初始化联合体中除第一个元素以外的其它元素,而不必使用一些技巧,比如计算出浮点数实现定义的整数等价值。 - Adam Rosenfield
1
我并不要求初始化多个成员,只是不想每次都初始化第一个成员。这比使用等效物件更复杂的原因是其中一些成员是联合体中更复杂的成员。我可以指向我们的源代码(修改后的Lua以在小型设备上运行),但需要一些挖掘才能理解其工作原理。如果有兴趣,这里是一个起点:http://svn.berlios.de/svnroot/repos/elua/trunk/src/lua/lrotable.h 描述(是的,它显示为原始格式,文档尚未在其他地方出现):http://svn.berlios.de/svnroot/repos/elua/trunk/doc/en/arch_ltr.html - James Snyder
如果你想将它编译为标准的C++,恐怕你需要使用像“等效物”这样的黑科技。而且你只需要在进入ROM的数据中使用它们,在进入RAM的所有其他数据中,你可以像其他人指出的那样创建常规构造函数。 - lothar

0

干井报告:

鉴于

struct S {
  int mA;
  int mB;
  S() {}
  S(int b} : mB(b) {} // a ctor that does partial initialization
};

我尝试从S中派生出S1,其中S1的内联默认构造函数调用S(int)并传递硬编码值...

struct S1 {
  S1() : S(22) {}
} s1;

...然后使用gcc 4.0.1 -O2 -S进行编译。希望优化器能够看到s1.mB必须是22,并在编译时分配它的值,但从汇编代码来看...

    movl    $22, 4+_s1-"L00000000002$pb"(%ebx)

……看起来生成的代码在 main 函数运行之前会进行初始化。即使它能够工作,也不可能通过 C99 的编译,并且每个你想要初始化的对象都需要派生一个类,这是一个笨拙而无用的做法,所以不必费心。


1
谢谢。它不一定要编译为C99。初始化程序在许多地方,但是它们使用一些宏定义,因此如果我可以使用最小的修改来ifdef __cplusplus,那也可以。 - James Snyder

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