ChatGPT解决这个技术问题 Extra ChatGPT

方便的 C++ 结构初始化

我正在尝试找到一种方便的方法来初始化“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 */ 或更好的东西?或者,我可以解释为什么我不应该这样做(即为什么我的心理范式不好)。

编辑

方便,我的意思是可维护和非冗余。

我认为 B 示例与您将要获得的一样接近。
我不明白示例 B 是如何“糟糕的风格”。这对我来说很有意义,因为您正在使用它们各自的值依次初始化每个成员。
迈克,这是一种糟糕的风格,因为不清楚哪个价值归哪个成员。您必须查看结构的定义,然后计算成员以找出每个值的含义。
另外,如果 FooBar 的定义在未来发生变化,初始化可能会被破坏。
如果初始化变得漫长而复杂,不要忘记构建器模式

i
iammilind

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

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

至少在一定程度上有所帮助。


+1:它并不能真正确保正确的初始化(来自编译器 POV),但确实可以帮助读者……尽管注释应该保持同步。
如果我以后在 foobar 之间插入新字段,注释不会阻止结构的初始化被破坏。 C 仍然会初始化我们想要的字段,但 C++ 不会。这就是问题的重点——如何在 C++ 中实现相同的结果。我的意思是,Python 使用命名参数,C - 使用“命名”字段,C++ 也应该有一些东西,我希望。
评论同步?让我休息一下。安全通过窗户。重新排序参数和繁荣。 explicit FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar) 会好很多。注意 explicit 关键字。在安全方面,即使打破标准也更好。在 Clang 中:-Wno-c99-extensions
@DanielW,这不是关于什么更好或什么不是。这个答案是根据 OP 不想要样式 A(不是 c++)、B 或 C,它涵盖了所有有效的情况。
@iammilind我认为暗示为什么OP的心理范式不好可以改善答案。我认为这就像现在一样危险。
i
ivaigult

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

2021 年更新

正如@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 的完整和最终实现提供。
20201 年?天哪,我小睡了很久!
e
eyelash

您可以使用 lambda:

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

有关此成语的更多信息,请参见 Herb Sutter's blog


这种方法初始化字段两次。一旦进入构造函数。第二个是fb.XXX = YYY
r
ralphtheninja

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

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

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

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

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

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

您的问题有些困难,因为即使是功能:

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 样式参数名称的支持)。
M
Matthias Urlichs

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


这不符合 C++ 标准!
我知道这是不标准的。但是如果你可以使用它,它仍然是初始化一个结构体的最明智的方式。
您可以保护 x 和 y 的类型,将错误的构造函数设为私有:private: FooBar(float x, int y) {};
clang(基于 llvm 的 c++ 编译器)也支持这种语法。太糟糕了,它不是标准的一部分。
我们都知道 C 初始化程序不是 C++ 标准的一部分。但是许多编译器确实理解它,并且问题没有说明针对的是哪个编译器(如果有的话)。因此,请不要对这个答案投反对票。
p
parapura rajkumar

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 标记设置器!
J
John

选项 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。
M
Max Vollmer

我知道这个问题很老,但是有一种方法可以解决这个问题,直到 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 提案?
Ö
Öö Tiib

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

如果你想用名字来表示参数,那么你可以使用 boost parameter library,但它可能会使不熟悉它的人感到困惑。

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


我称之为糟糕的风格,因为我认为它是零可维护的。如果我在一年内添加另一个成员怎么办?或者如果我更改成员的排序/类型?初始化它的每一段代码都可能(很可能)中断。
@bitmask但是只要你没有命名参数,你也必须更新构造函数调用,我认为没有多少人认为构造函数是不可维护的坏风格。我也认为命名初始化不是C,而是C99,其中C++绝对不是超集。
如果您在一年内将另一个成员添加到结构的末尾,那么它将在现有代码中默认初始化。如果您对它们重新排序,那么您必须编辑所有现有代码,无事可做。
@bitmask:第一个例子也是“不可维护的”。如果你重命名结构中的变量会发生什么?当然,您可以进行全部替换,但这可能会意外重命名不应重命名的变量。
@ChristianRau 从什么时候开始 C99 不是 C? C组和C99不是特定版本/ ISO规范吗?
c
cls

这个语法怎么样?

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= 返回。所以,实际上你初始化了两次类成员。
E
Emanuele Pavanello

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

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

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


N
Nelson

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


u
user3333852

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

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

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


这没有提供问题的答案。一旦你有足够的reputation,你就可以comment on any post;相反,provide answers that don't require clarification from the asker。 - From Review