方便C++结构初始化

Convenient C++ struct initialisation

本文关键字:初始化 结构 C++ 方便      更新时间:2023-10-16

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

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++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;
}

海湾合作委员会演示MSVC 演示

2021 年更新

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

既然C++中不允许style A并且您不想要style B那么使用style BX怎么样:

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

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

你可以使用 lambda:

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

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

变量提取到描述它们的函数中(基本重构(:

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初始值设定项语法。如果可以,只需使用该方法即可。

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++一样。如果你想要命名参数,目标 C 应该是更好的选择。

选项 E:

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

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

我知道

这个问题很老,但是有一种方法可以解决这个问题,直到 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参数库,但它可能会使不熟悉它的人感到困惑。

重新排序

结构成员就像重新排序函数参数一样,如果你不小心地进行这种重构,可能会导致问题。

这个语法呢?

typedef struct
{
    int a;
    short b;
}
ABCD;
ABCD abc = { abc.a = 5, abc.b = 7 };

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

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

#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 一起使用。但另一方面,它干净、简单、纯净、标准。

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

正如你上面所说,小缺点是不能立即看到哪个参数是哪个成员,但如果将鼠标悬停在代码上,大多数 IDE 都会在这里提供帮助。

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