我正在尝试找到一种方便的方法来初始化“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 */
示例一样糟糕,因为它没有明确说明哪个值分配给哪个成员。
所以,我的问题基本上是如何在 C++ 中实现类似于 /* A */
或更好的东西?或者,我可以解释为什么我不应该这样做(即为什么我的心理范式不好)。
编辑
方便,我的意思是可维护和非冗余。
由于 C++ 中不允许使用 style A
并且您不想要 style B
那么使用 style BX
怎么样:
FooBar fb = { /*.foo=*/ 12, /*.bar=*/ 3.4 }; // :)
至少在一定程度上有所帮助。
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;
}
2021 年更新
正如@Code Doggo 所述,使用 Visual Studio 2019 的任何人都需要为 Configuration Properties -> C/C++ -> Language
下包含的“C++ 语言标准”字段设置 /std:c++latest
。
Configuration Properties -> C/C++ -> Language
下包含的“C++ 语言标准”字段设置 /std:c++latest
。这将提供对当前正在开发中的 C++20 功能的访问。 C++20 尚未作为 Visual Studio 的完整和最终实现提供。
您可以使用 lambda:
const FooBar fb = [&] {
FooBar fb;
fb.foo = 12;
fb.bar = 3.4;
return fb;
}();
有关此成语的更多信息,请参见 Herb Sutter's blog。
fb.XXX = YYY
。
将内容提取到描述它们的函数中(基本重构):
FooBar fb = { foo(), bar() };
我知道样式非常接近您不想使用的样式,但它可以更轻松地替换常量值并解释它们(因此不需要编辑注释),如果它们改变的话。
您可以做的另一件事(因为您很懒惰)是使构造函数内联,因此您不必输入太多内容(删除“Foobar::”以及在 h 和 cpp 文件之间切换所花费的时间):
struct FooBar {
FooBar(int f, float b) : foo(f), bar(b) {}
int foo;
float bar;
};
您的问题有些困难,因为即使是功能:
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++ 前端(包括 GCC 和 clang)都理解 C 初始化语法。如果可以,只需使用该方法即可。
private: FooBar(float x, int y) {};
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);
inline
标记设置器!
选项 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++。命名参数。
FooBar fb = {};
而不是 memset,它默认初始化所有结构成员。
我知道这个问题很老,但是有一种方法可以解决这个问题,直到 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);\
/* B */
在 C++ 中的方式很好,C++0x 也将扩展语法,因此它对 C++ 容器也很有用。我不明白你为什么称它为坏风格?
如果你想用名字来表示参数,那么你可以使用 boost parameter library,但它可能会使不熟悉它的人感到困惑。
重新排序 struct 成员就像重新排序函数参数一样,如果你不仔细进行这种重构可能会导致问题。
这个语法怎么样?
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++ 吗?
ABCD abc = { abc.b = 7, abc.a = 5 };
。
对我来说,允许内联初始化的最懒惰的方法是使用这个宏。
#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 成员在代码中初始化为合理值的最实用的方法。
正如您在上面所说,一个小缺点是人们不会立即看到哪个成员是什么参数,但是如果将鼠标悬停在代码上,大多数 IDE 都会在这里提供帮助。
我认为更有可能的是添加了新成员,在这种情况下,我希望结构的所有构造都无法编译,因此开发人员被迫进行审查。在我们相当大的代码库中,这已经证明了自己,因为它指导开发人员需要注意什么,从而创建自我维护的代码。
foo
和bar
之间插入新字段,注释不会阻止结构的初始化被破坏。 C 仍然会初始化我们想要的字段,但 C++ 不会。这就是问题的重点——如何在 C++ 中实现相同的结果。我的意思是,Python 使用命名参数,C - 使用“命名”字段,C++ 也应该有一些东西,我希望。explicit FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar)
会好很多。注意 explicit 关键字。在安全方面,即使打破标准也更好。在 Clang 中:-Wno-c99-extensions