方便的C ++结构初始化


140

我正在尝试寻找一种方便的方法来初始化“ pod” C ++结构。现在,考虑以下结构:

struct FooBar {
  int foo;
  float bar;
};
// just to make all examples work in C and C++:
typedef struct FooBar FooBar;

如果我想方便地用C(!)初始化它,我可以简单地写:

/* A */ FooBar fb = { .foo = 12, .bar = 3.4 }; // illegal C++, legal C

请注意,我想明确地避免使用以下表示法,因为如果将来我在结构中进行任何更改,它都会令我不寒而栗:

/* B */ FooBar fb = { 12, 3.4 }; // legal C++, legal C, bad style?

为了在C ++中实现与/* A */示例相同(或至少相似)的效果,我将必须实现一个愚蠢的构造函数:

FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar) {}
// ->
/* C */ FooBar fb(12, 3.4);

哪种对开水有益,但不适合懒人(懒惰是件好事,对吗?)。同样,它与/* B */示例一样糟糕,因为它没有明确说明哪个值分配给哪个成员。

因此,我的问题基本上是我如何才能实现与/* A */C ++ 类似或更好的东西?另外,我可以解释一下为什么我不想这样做(即为什么我的思维模式不好)。

编辑

通过方便的,我的意思也是维护非冗余


2
我认为B示例与您将要得到的非常接近。
马龙

2
我看不到示例B的“坏风格”。这对我来说很有意义,因为您依次使用各自的值初始化每个成员。
麦克·贝利

25
迈克,这是不好的作风,因为不清楚哪个值分配给哪个成员。您必须查看结构的定义,然后对成员进行计数以找到每个值的含义。
jnnnnn

9
另外,如果将来更改FooBar的定义,则初始化可能会被破坏。
爱德华·福尔克

如果初始化变得冗长而复杂,请不要忘记构建器模式
雪橇

Answers:


20

指定的初始化将在c ++ 2a中受支持,但您不必等待,因为GCC,Clang和MSVC 正式支持它们。

#include <iostream>
#include <filesystem>

struct hello_world {
    const char* hello;
    const char* world;
};

int main () 
{
    hello_world hw = {
        .hello = "hello, ",
        .world = "world!"
    };

    std::cout << hw.hello << hw.world << std::endl;
    return 0;
}

GCC Demo MSVC Demo


警告免责声明:请记住,如果稍后在结构的末尾添加参数,则旧的初始化仍会静默编译,而无需初始化。
Catskul

1
@Catskul编号。它将使用空的初始化程序列表进行初始化,这将导致初始化为零。
ivaigult19年

你是对的。谢谢。我应该澄清一下,其余参数将被默默地默认初始化。我要说的是,任何希望这样做可能有助于强制执行POD类型的完全显式初始化的人都会感到失望。
Catskul

43

由于style AC ++中不允许使用,因此您不希望style B使用style BX

FooBar fb = { /*.foo=*/ 12, /*.bar=*/ 3.4 };  // :)

至少在某种程度上有所帮助。


8
+1:它不能真正确保正确的初始化(从编译器POV进行),但可以肯定对读者有帮助...尽管注释应保持同步。
Matthieu M.

18
如果在之间foo以及bar将来插入新字段,则注释不会阻止破坏结构的初始化。C仍然会初始化我们想要的字段,但是C ++不会。这就是问题的重点-如何在C ++中获得相同的结果。我的意思是,Python使用命名参数来执行此操作,使用C-使用“命名”字段来执行此操作,我希望C ++也应该具有某些功能。
dmitry_romanov

2
评论同步吗?休息一下 安全通过窗户。重新排列参数和动臂。更好explicit FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar) 。注意显式关键字。就安全而言,即使打破标准也更好。在Clang中:-Wno-c99-extensions
Daniel O

@DanielW,这不是什么更好还是什么不好。这个答案是因为OP不需要涵盖所有有效情况的样式A(不是c ++),样式B或样式C。
iammilind

@iammilind我认为关于OP的思维模式为何不好的暗示可以改善答案。我认为这很危险。
戴斯特2015年

10

您可以使用lambda:

const FooBar fb = [&] {
    FooBar fb;
    fb.foo = 12;
    fb.bar = 3.4;
    return fb;
}();

有关该惯用语的更多信息,请参见Herb Sutter的博客


1
这种方法将字段初始化两次。一旦在构造函数中。其次是fb.XXX = YYY
Dmytro Ovdiienko

9

将竞争对象提取到描述它们的函数中(基本重构):

FooBar fb = { foo(), bar() };

我知道该样式与您不希望使用的样式非常接近,但是它可以更轻松地替换常量值,并且可以对常量值进行解释(因此无需编辑注释)(如果更改了)。

您可以做的另一件事(因为您很懒惰)是使构造函数内联,因此您不必键入太多内容(删除“ Foobar ::”以及在h和cpp文件之间切换所花费的时间):

struct FooBar {
  FooBar(int f, float b) : foo(f), bar(b) {}
  int foo;
  float bar;
};

1
如果您想要做的就是能够使用一组值快速初始化结构,我强烈建议其他人阅读此问题以选择此答案的底部代码片段的样式。
kayleeFrye_onDeck

8

您的问题有些困难,因为即使函数:

static FooBar MakeFooBar(int foo, float bar);

可以称为:

FooBar fb = MakeFooBar(3.4, 5);

因为内置数字类型的升级和转换规则。(C从未真正过强类型化)

在C ++中,尽管借助模板和静态断言,您仍可以实现所需的结果:

template <typename Integer, typename Real>
FooBar MakeFooBar(Integer foo, Real bar) {
  static_assert(std::is_same<Integer, int>::value, "foo should be of type int");
  static_assert(std::is_same<Real, float>::value, "bar should be of type float");
  return { foo, bar };
}

在C语言中,您可以命名参数,但是您将永远无法更进一步。

另一方面,如果您想要的只是命名参数,那么您将编写许多繁琐的代码:

struct FooBarMaker {
  FooBarMaker(int f): _f(f) {}
  FooBar Bar(float b) const { return FooBar(_f, b); }
  int _f;
};

static FooBarMaker Foo(int f) { return FooBarMaker(f); }

// Usage
FooBar fb = Foo(5).Bar(3.4);

而且,如果您愿意,您可以加入类型促进保护功能。


1
“在C ++中,您想要的是可以实现的”:OP是否要求帮助防止参数顺序混淆?您提出的模板将如何实现?为了简单起见,假设我们有2个参数,它们都是int。
最多

@max:仅当类型不同(即使它们可以相互转换)时才阻止它,这是OP的情况。如果它不能区分类型,那当然是行不通的,但这是另一个问题。
Matthieu M.

知道了 是的,这是两个不同的问题,我想第二个问题目前在C ++中还没有一个好的解决方案(但是C ++ 20似乎在聚合初始化中添加了对C99样式参数名称的支持)。
最多

6

许多编译器的C ++前端(包括GCC和clang)都了解C初始化程序语法。如果可以,只需使用该方法。


16
哪个不符合C ++标准!
bitmask

5
我知道这是非标准的。但是,如果可以使用它,它仍然是初始化结构的最明智的方法。
Matthias Urlichs,2013年

2
您可以保护x和y的类型,使错误的构造函数private: FooBar(float x, int y) {};
变为

4
clang(基于llvm的c ++编译器)也支持此语法。太糟糕了,它不是标准的一部分。
nimrodm

我们都知道C初始化程序不是C ++标准的一部分。但是许多编译器确实了解它,而问题并没有说明要针对哪个编译器(如果有)。因此,请不要拒绝这个答案。
Matthias Urlichs '18

4

C ++的另一种方式是

struct Point
{
private:

 int x;
 int y;

public:
    Point& setX(int xIn) { x = Xin; return *this;}
    Point& setY(int yIn) { y = Yin; return *this;}

}

Point pt;
pt.setX(20).setY(20);

2
对于函数式编程(例如,在函数调用的参数列表中创建对象)来说很繁琐,但实际上确实是个好主意!
bitmask

27
优化程序可能会减少它,但我的眼睛没有。
Matthieu M.

6
两个词:啊...啊!与在'Point pt中使用公共数据相比,这有什么好处?pt.x = pt.y = 20;`?或者,如果要封装,这比构造函数好吗?
OldPeculier 2012年

3
它比构造函数要好,因为您必须查看参数顺序的构造函数声明...是x,y还是y,x,但我在呼叫站点上展示的方式很明显
parapura rajkumar

2
如果您想要一个const结构,这将不起作用。或者如果您想告诉编译器不要允许未初始化的结构。如果您真的想用这种方法,请至少用inline!标记设置者。
Matthias Urlichs,2015年

3

选项D:

FooBar FooBarMake(int foo, float bar)

合法C,合法C ++。轻松优化POD。当然没有命名参数,但这就像所有C ++一样。如果要使用命名参数,则目标C应该是更好的选择。

选项E:

FooBar fb;
memset(&fb, 0, sizeof(FooBar));
fb.foo = 4;
fb.bar = 15.5f;

合法C,合法C ++。命名参数。


12
除了可以FooBar fb = {};在C ++中使用memset之外,它还可以默认初始化所有结构成员。
二OO Tiib

@ÖöTiib:不幸的是,这是非法的C。
CB Bailey

3

我知道这个问题很古老,但是有一种解决方法,直到C ++ 20最终将此功能从C引入C ++。解决此问题的方法是,将预处理器宏与static_asserts结合使用以检查初始化是否有效。(我知道宏通常是不好的,但是在这里我看不到其他方法。)请参见下面的示例代码:

#define INVALID_STRUCT_ERROR "Instantiation of struct failed: Type, order or number of attributes is wrong."

#define CREATE_STRUCT_1(type, identifier, m_1, p_1) \
{ p_1 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\

#define CREATE_STRUCT_2(type, identifier, m_1, p_1, m_2, p_2) \
{ p_1, p_2 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\

#define CREATE_STRUCT_3(type, identifier, m_1, p_1, m_2, p_2, m_3, p_3) \
{ p_1, p_2, p_3 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_3) >= (offsetof(type, m_2) + sizeof(identifier.m_2)), INVALID_STRUCT_ERROR);\

#define CREATE_STRUCT_4(type, identifier, m_1, p_1, m_2, p_2, m_3, p_3, m_4, p_4) \
{ p_1, p_2, p_3, p_4 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_3) >= (offsetof(type, m_2) + sizeof(identifier.m_2)), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_4) >= (offsetof(type, m_3) + sizeof(identifier.m_3)), INVALID_STRUCT_ERROR);\

// Create more macros for structs with more attributes...

然后,当您具有带有const属性的结构时,可以执行以下操作:

struct MyStruct
{
    const int attr1;
    const float attr2;
    const double attr3;
};

const MyStruct test = CREATE_STRUCT_3(MyStruct, test, attr1, 1, attr2, 2.f, attr3, 3.);

这有点不方便,因为您需要为每个可能数量的属性使用宏,并且需要在宏调用中重复实例的类型和名称。同样,您不能在return语句中使用宏,因为断言是在初始化之后进行的。

但这确实可以解决您的问题:更改结构时,调用将在编译时失败。

如果使用C ++ 17,甚至可以通过强制使用相同的类型来使这些宏更加严格,例如:

#define CREATE_STRUCT_3(type, identifier, m_1, p_1, m_2, p_2, m_3, p_3) \
{ p_1, p_2, p_3 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_3) >= (offsetof(type, m_2) + sizeof(identifier.m_2)), INVALID_STRUCT_ERROR);\
static_assert(typeid(p_1) == typeid(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(typeid(p_2) == typeid(identifier.m_2), INVALID_STRUCT_ERROR);\
static_assert(typeid(p_3) == typeid(identifier.m_3), INVALID_STRUCT_ERROR);\

是否有C ++ 20建议书允许命名的初始化程序?
尼尔·尼森


2

/* B */在C ++中,这种方式很好,而且C ++ 0x会扩展语法,因此对C ++容器也很有用。我不明白你为什么称它为不良风格?

如果您想用名称来表示参数,则可以使用boost参数库,但这可能会使不熟悉它的人感到困惑。

对结构成员进行重新排序就像对函数参数进行重新排序一样,如果您不十分仔细地进行重构,则可能会导致问题。


7
我称其为不好的风格,因为我认为它是零可维护的。如果我一年内再增加一个成员该怎么办?或者,如果我更改会员的订购/类型?初始化它的每段代码都可能(很可能)中断。
bitmask

2
@bitmask但是,只要您没有命名参数,就必须更新构造函数调用,而且我认为没有多少人认为构造函数是无法维护的不良样式。我还认为命名初始化不是C,而是C99,其中C ++绝对不是超集。
克里斯汀·劳

2
如果您在一年后将另一个成员添加到该结构的末尾,则它将在现有代码中进行默认初始化。如果对它们重新排序,则必须编辑所有现有代码,无需执行任何操作。
二OO Tiib

1
@bitmask:第一个示例也将是“ untaintainable”。如果在结构中重命名变量,会发生什么?当然,您可以执行全部替换,但是这可能会意外地重命名不应重命名的变量。
麦克·贝利

@ChristianRau什么时候C99不是C?C组和C99是否不是特定的版本/ ISO规范?
altendky 2014年

1

那这种语法呢?

typedef struct
{
    int a;
    short b;
}
ABCD;

ABCD abc = { abc.a = 5, abc.b = 7 };

刚刚在Microsoft Visual C ++ 2015和g ++ 6.0.2上进行了测试。工作正常。
如果要避免重复变量名称,也可以创建特定的宏。


clang++3.5.0-10与-Weverything -std=c++1z似乎可以确认这一点。但这看起来并不正确。您知道标准在哪里确认这是有效的C ++吗?
位掩码

我不知道,但是很久以前我就在不同的编译器中使用了它,并且没有发现任何问题。现在已在g ++ 4.4.7上进行了测试-正常工作。
cls 2016年

5
我不认为这项工作。尝试ABCD abc = { abc.b = 7, abc.a = 5 };
raymai97

@deselect,它起作用,因为字段是用operator =返回的值初始化的。因此,实际上您两次初始化类成员。
Dmytro Ovdiienko

1

对我来说,允许内联inizialization的最懒的方法是使用此宏。

#define METHOD_MEMBER(TYPE, NAME, CLASS) \
CLASS &set_ ## NAME(const TYPE &_val) { NAME = _val; return *this; } \
TYPE NAME;

struct foo {
    METHOD_MEMBER(string, attr1, foo)
    METHOD_MEMBER(int, attr2, foo)
    METHOD_MEMBER(double, attr3, foo)
};

// inline usage
foo test = foo().set_attr1("hi").set_attr2(22).set_attr3(3.14);

该宏创建属性和自引用方法。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.