方便的 C++ 结构初始化
Posted
技术标签:
【中文标题】方便的 C++ 结构初始化【英文标题】:Convenient C++ struct initialisation 【发布时间】:2011-09-05 02:52:34 【问题描述】:我正在尝试找到一种方便的方法来初始化“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
请注意,我想明确避免使用以下符号,因为如果我将来更改结构中的 anything,我会觉得它会折断我的脖子:
/* 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 */
或更好的东西?
或者,我可以解释为什么我不应该这样做(即为什么我的心理范式不好)。
编辑
方便指的是可维护和非冗余。
【问题讨论】:
我认为 B 示例与您将获得的一样接近。 我看不出示例 B 的“风格不好”。这对我来说很有意义,因为您正在使用它们各自的值依次初始化每个成员。 迈克,这是一种糟糕的风格,因为不清楚哪个值属于哪个成员。您必须查看结构的定义,然后计算成员以找出每个值的含义。 另外,如果 FooBar 的定义在未来发生变化,初始化可能会被破坏。 如果初始化变得漫长而复杂,不要忘记构建器模式 【参考方案1】:由于 C++ 中不允许使用 style A
并且您不想要 style B
那么使用 style BX
怎么样:
FooBar fb = /*.foo=*/ 12, /*.bar=*/ 3.4 ; // :)
至少在某种程度上有所帮助。
【讨论】:
+1:它并不能真正确保正确初始化(来自编译器 POV),但确实可以帮助读者……尽管 cmets 应该保持同步。 如果我以后在foo
和bar
之间插入新字段,注释不会阻止结构的初始化被破坏。 C 仍然会初始化我们想要的字段,但 C++ 不会。这就是问题的重点——如何在 C++ 中实现相同的结果。我的意思是,Python 使用命名参数,C - 使用“命名”字段,C++ 也应该有一些东西,我希望。
评论同步?让我休息一下。安全通过窗户。重新排序参数和繁荣。 explicit FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar)
好多了。注意 explicit 关键字。在安全方面,即使打破标准也更好。在 Clang 中:-Wno-c99-extensions
@DanielW,这不是关于什么更好或什么不是。这个答案是因为 OP 不想要 Style A(不是 c++)、B 或 C,它涵盖了所有有效的情况。
@iammilind 我认为提示为什么 OP 的 心理范式不好 可以改善答案。我认为现在这样很危险。【参考方案2】:
c++2a 将支持指定初始化,但您不必等待,因为它们是 GCC、Clang 和 MSVC 的 officialy supported。
#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
20201 年更新
正如 @Code Doggo 所述,使用 Visual Studio 2019 的任何人都需要为 Configuration Properties -> C/C++ -> Language
下包含的“C++ 语言标准”字段设置 /std:c++latest
。
【讨论】:
注意事项:请记住,如果您稍后将参数添加到结构的末尾,旧的初始化仍然会在未初始化的情况下静默编译。 @Catskul 否。它 will be initialized 带有空的初始化列表,这将导致初始化为零。 你是对的。谢谢你。我应该澄清一下,其余参数将被有效地默认初始化。我的意思是,任何希望这可能有助于强制执行 POD 类型的完全显式初始化的人都会失望。 自 2020 年 12 月 31 日起,使用 Visual Studio 2019 的任何人都需要为Configuration Properties -> C/C++ -> Language
下包含的“C++ 语言标准”字段设置 /std:c++latest
。这将提供对当前正在开发中的 C++20 功能的访问。 C++20 尚未作为 Visual Studio 的完整和最终实现提供。【参考方案3】:
您可以使用 lambda:
const FooBar fb = [&]
FooBar fb;
fb.foo = 12;
fb.bar = 3.4;
return fb;
();
有关此成语的更多信息,请访问Herb Sutter's blog。
【讨论】:
这种方法初始化字段两次。一旦进入构造函数。第二个是fb.XXX = YYY
。【参考方案4】:
将内容提取到描述它们的函数中(基本重构):
FooBar fb = foo(), bar() ;
我知道该样式与您不想使用的样式非常接近,但它可以更轻松地替换常量值并解释它们(因此不需要编辑 cmets),如果它们发生变化的话。
您可以做的另一件事(因为您很懒惰)是使构造函数内联,因此您不必输入太多内容(删除“Foobar::”以及在 h 和 cpp 文件之间切换所花费的时间):
struct FooBar
FooBar(int f, float b) : foo(f), bar(b)
int foo;
float bar;
;
【讨论】:
如果您想要做的只是能够使用一组值。【参考方案5】:你的问题有点难,因为即使是函数:
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);
如果你愿意,你可以加入类型提升保护。
【讨论】:
“在 C++ 中,你想要的是可以实现的”:OP 不是要求帮助防止参数顺序的混淆吗?您提出的模板如何实现这一目标?为简单起见,假设我们有 2 个参数,它们都是 int。 @max:只有当类型不同时才会阻止它(即使它们可以相互转换),这是 OP 的情况。如果它不能区分类型,那么它当然不起作用,但这是另一个问题。 啊,明白了。是的,这是两个不同的问题,我猜第二个问题目前在 C++ 中没有很好的解决方案(但似乎 C++ 20 在聚合初始化中添加了对 C99 样式参数名称的支持)。 【参考方案6】:许多编译器的 C++ 前端(包括 GCC 和 clang)都理解 C 初始化语法。如果可以,只需使用该方法即可。
【讨论】:
不符合C++标准! 我知道这是非标准的。但是如果你能用它,它仍然是初始化一个结构体的最明智的方式。 您可以保护 x 和 y 的类型,将错误的构造函数设为私有:private: FooBar(float x, int y) ;
clang(基于 llvm 的 c++ 编译器)也支持这种语法。太糟糕了,它不是标准的一部分。
我们都知道 C 初始化器不是 C++ 标准的一部分。但是许多编译器确实理解它,并且问题没有说明针对的是哪个编译器(如果有的话)。因此,请不要对这个答案投反对票。【参考方案7】:
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);
【讨论】:
函数式编程很麻烦(即在函数调用的参数列表中创建对象),但在其他方面确实是个好主意! 优化器可能会减少它,但我的眼睛没有。 两个字:啊……啊!这比使用带有'Point pt; 的公共数据更好吗? pt.x = pt.y = 20;`?或者如果你想要封装,这比构造函数更好吗? 它比构造函数更好,因为您必须查看参数顺序的构造函数声明......是 x , y 还是 y , x 但我展示的方式在调用时很明显网站 如果你想要一个 const 结构,这不起作用。或者如果你想告诉编译器不允许未初始化的结构。如果你真的想这样做,至少用inline
标记setter!【参考方案8】:
选项 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++ 中使用FooBar fb = ;
而不是 memset,它默认初始化所有结构成员。
@ÖöTiib:不幸的是,这是非法的 C。【参考方案9】:
我知道这个问题很老了,但是在 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 提议允许命名初始化器? @MaëlNison 是的:Designated initializers (since C++20)【参考方案10】:/* B */
在 C++ 中的方式很好,而且 C++0x 将扩展语法,因此它对 C++ 容器也很有用。我不明白你为什么称它为坏风格?
如果你想用名字来表示参数,那么你可以使用boost parameter library,但它可能会让不熟悉的人感到困惑。
重新排序结构成员就像重新排序函数参数,如果你不仔细进行这种重构可能会导致问题。
【讨论】:
我称它为坏风格,因为我认为它是零可维护的。如果我在一年内添加另一个成员怎么办?或者如果我更改成员的排序/类型?初始化它的每一段代码都可能(很可能)中断。 @bitmask 但是只要你没有命名参数,你也必须更新构造函数调用,我认为没有多少人认为构造函数是不可维护的坏风格。我也觉得命名初始化不是C,而是C99,其中C++绝对不是超集。 如果您在一年内将另一个成员添加到结构的末尾,那么它将在现有代码中默认初始化。如果您重新排序它们,那么您必须编辑所有现有代码,无事可做。 @bitmask:第一个例子也是“不可维护的”。如果你重命名结构中的变量会发生什么?当然,您可以执行全部替换,但这可能会意外重命名不应重命名的变量。 @ChristianRau 从什么时候开始 C99 不是 C? C 组和 C99 不是特定版本/ISO 规范吗?【参考方案11】:这个语法怎么样?
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 上测试 - 工作正常。
我认为这行不通。试试ABCD abc = abc.b = 7, abc.a = 5 ;
。
@deselect,它起作用是因为字段是用值初始化的,由 operator= 返回。所以,实际上你初始化了两次类成员。【参考方案12】:
对我来说,允许内联初始化的最懒惰的方法是使用这个宏。
#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);
那个宏创建属性和自引用方法。
【讨论】:
【参考方案13】:对于 C++20 之前的 C++ 版本(引入了命名初始化,使您的选项 A 在 C++ 中有效),请考虑以下事项:
int main()
struct TFoo int val; ;
struct TBar float val; ;
struct FooBar
TFoo foo;
TBar bar;
;
FooBar mystruct = TFoo12, TBar3.4 ;
std::cout << "foo = " << mystruct.foo.val << " bar = " << mystruct.bar.val << std::endl;
请注意,如果您尝试使用 FooBar mystruct = TFoo12, TFoo3.4 ;
初始化结构,您将收到编译错误。
缺点是你必须为你的主结构中的每个变量创建一个额外的结构,而且你必须使用 mystruct.foo.val
的内部值。但另一方面,它干净、简单、纯粹和标准。
【讨论】:
以上是关于方便的 C++ 结构初始化的主要内容,如果未能解决你的问题,请参考以下文章