方便的 C + + 结构初始化

我试图找到一种方便的方法来初始化‘ pod’C + + structs:

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 */示例一样糟糕,因为它没有明确说明哪个值指向哪个成员。

那么,我的问题基本上就是如何在 C + + 中实现类似于 /* A */或者更好的东西? 或者,我可以解释为什么我不应该这样做(即为什么我的心理范式是坏的)。

剪辑

我说的 很方便也是指 可以维持不是多余的

155140 次浏览

在 C + + 中使用 /* B */的方法很好,而且 C + + 0x 将扩展语法,所以它对 C + + 容器也很有用。我不明白你为什么称之为坏的风格?

如果您希望用名称指示参数,那么可以使用 提升参数库提升参数库,但它可能会使不熟悉它的人感到困惑。

对结构成员重新排序就像对函数参数重新排序一样,如果不小心的话,这样的重构可能会导致问题。

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);

选择 D:

FooBar FooBarMake(int foo, float bar)

法律 C,法律 C + + 。POD 容易优化。当然没有命名参数,但是这就像所有的 C + + 一样。如果您想要命名参数,那么 Objective C 应该是更好的选择。

选项 E:

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

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

既然在 C + + 中不允许使用 style A,而且你也不想使用 style B,那么使用 style BX怎么样:

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

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

你的问题有点难,因为即使是函数:

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);

如果你喜欢的话,你可以在类型促进保护胡椒粉。

将内容提取到描述它们的函数中(基本重构) :

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

我知道样式与您不想使用的样式非常接近,但是它可以更容易地替换常量值并解释它们(因此不需要编辑注释) ,如果它们发生变化的话。

另一件你可以做的事情(因为你很懒)是让构造函数内联,这样你就不必输入太多(删除“ Foobar: :”和在 h 和 cpp 文件之间切换所花费的时间) :

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

许多编译器的 C + + 前端(包括 GCC 和 clang)理解 C 初始化器语法。

这个语法怎么样?

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


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

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

我知道这个问题已经很老了,但是有一种方法可以解决这个问题,直到 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);\

你可以用 lambda:

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

关于这个习语的更多信息可以在 Herb Sutter 的博客上找到。

在 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

2021年最新情况

正如 @ Code Doggo所指出的,任何使用 Visual Studio 2019的人都需要将 /std:c++latest设置为包含在 Configuration Properties -> C/C++ -> Language下的“ C + + 语言标准”字段。

对我来说,允许内联初始化的最懒惰的方法是使用这个宏。

#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);

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

对于 C + + 20之前的 C + + 版本(它引入了命名初始化,使您的选项 A 在 C + + 中有效) ,请考虑以下内容:

int main()
{
struct TFoo { int val; };
struct TBar { float val; };


struct FooBar {
TFoo foo;
TBar bar;
};


FooBar mystruct = { TFoo{12}, TBar{3.4} };


std::cout << "foo = " << mystruct.foo.val << " bar = " << mystruct.bar.val << std::endl;


}

注意,如果尝试用 FooBar mystruct = { TFoo{12}, TFoo{3.4} };初始化结构,将得到一个编译错误。

缺点是必须为主结构中的每个变量创建一个额外的结构,而且必须在 mystruct.foo.val中使用内部值。但另一方面,它是干净的,简单的,纯粹的和标准的。

我个人发现,在 struct 中使用构造函数是确保 struct 成员在代码中初始化为合理值的最实用的方法。

如上所述,小的缺点是不能立即看到哪个 param 是哪个成员,但是如果悬停在代码上,大多数 IDE 在这里都有帮助。

我认为更有可能的是添加了新成员,在这种情况下,我希望结构的所有构造都无法编译,因此开发人员必须进行审查。在我们相当大的代码库中,这已经证明了自己,因为它指导开发人员需要注意什么,因此创建了自我维护的代码。