方便的 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 应该保持同步。 如果我以后在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 不想要 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 -&gt; C/C++ -&gt; Language 下包含的“C++ 语言标准”字段设置 /std:c++latest

【讨论】:

注意事项:请记住,如果您稍后将参数添加到结构的末尾,旧的初始化仍然会在未初始化的情况下静默编译。 @Catskul 否。它 will be initialized 带有空的初始化列表,这将导致初始化为零。 你是对的。谢谢你。我应该澄清一下,其余参数将被有效地默认初始化。我的意思是,任何希望这可能有助于强制执行 POD 类型的完全显式初始化的人都会失望。 自 2020 年 12 月 31 日起,使用 Visual Studio 2019 的任何人都需要为 Configuration Properties -&gt; C/C++ -&gt; 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++ 结构初始化的主要内容,如果未能解决你的问题,请参考以下文章

未初始化的 C++ 结构的行为

C++静态结构体数据成员的初始化

C++ 全局 静态结构体变量的初始化

C++ 结构初始化

C++ 结构体如何初始化

初始化指向结构 c++ 的 const 指针