方便的C++结构体初始化

171

我正在尝试找到一种方便的方法来初始化'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 */ 或更好的效果?
或者,我也可以接受一个解释,为什么不应该这样做(即为什么我的思维范式不好)。
编辑
方便是指易于维护和非冗余的。

2
我认为B示例是你能得到的最接近的。 - Marlon
2
我不明白为什么例子B是“不好的风格”。对我来说它很有意义,因为你在依次初始化每个成员变量及其相应的值。 - Mike Bailey
33
Mike,这种写法不好,因为不清楚哪个值对应哪个成员。你需要查看结构体的定义并计算成员数量,才能确定每个值的含义。 - jnnnnn
12
此外,如果FooBar的定义在未来发生变化,初始化可能会出现问题。 - Edward Falk
我在我的C++项目中实际上使用了"A"风格,并且在GCC下运行良好,然后我尝试使用MSVC构建它,但它并不完全喜欢它。因此,只要您不介意非交叉编译器,就可以使用它。 - Grishka
显示剩余2条评论
14个回答

49

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

GCC演示 MSVC演示

2021更新

正如@Code Doggo所指出的,任何使用Visual Studio 2019的人都需要在配置属性 -> C/C++ -> 语言下的“C++语言标准”字段中设置/std:c++latest


2
注意事项:请记住,如果您稍后向结构的末尾添加参数,则旧的初始化仍将在未经初始化的情况下静默编译。 - Catskul
2
@Catskul 不会。它将使用空初始化列表进行初始化,这将导致用零进行初始化。 - ivaigult
你说得对。谢谢。我应该澄清一下,剩余的参数将会被默认初始化而不会有任何提示。我的意思是,希望这可以帮助强制执行POD类型的完全显式初始化的人将会失望。 - Catskul
3
截至2020年12月31日,任何使用Visual Studio 2019的人都需要在“配置属性-> C / C ++ ->语言”下包含的“C ++语言标准”字段中设置“/std:c++latest”。这将提供对当前正在开发中的C ++20功能的访问。目前,Visual Studio尚未提供完整和最终实现的C ++20。 - Code Doggo
4
20201?哎呀,我睡了一个超长的小觉! - CaptainCodeman

47

既然在C++中不允许使用样式A,而您又不想使用样式B,那么使用样式BX如何:

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

至少在某种程度上提供帮助。


9
+1:从编译器的角度来看,这并不能确保正确的初始化,但肯定有助于读者理解……尽管注释应该与代码保持同步。 - Matthieu M.
25
如果我将来在foobar之间插入新字段,注释不能防止结构初始化被破坏。 C仍然会初始化我们想要的字段,但C ++不会。这就是问题的关键 - 如何在C ++中实现相同的结果。我的意思是,Python使用命名参数实现此功能,C使用“命名”字段,而C ++应该也有类似的东西,希望如此。 - dmitry_romanov
3
同步的评论?开什么玩笑。安全措施被抛到了脑后。重新排列参数,一切都妥妥的。使用 explicit FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar) 更好。请注意 explicit 关键字。甚至违反标准也比安全更好。在 Clang 中:-Wno-c99-extensions - Daniel O
@iammilind,我们为什么要在这里考虑“=”呢?根本没有必要! - Paiusco
@DanielO 当你试图进行静态初始化并且代码还不能运行的时候,这是毫无意义的,真是让人恼火! - Enerccio
显示剩余2条评论

13

你可以使用lambda表达式:

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

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


2
这种方法会导致字段被初始化两次。一次在构造函数中,另一次是 fb.XXX = YYY - Dmytro Ovdiienko
1
@DmytroOvdiienko 不会,复制省略将防止这种情况发生:https://en.cppreference.com/w/cpp/language/copy_elision - Moop
@Moop 我的意思是字段可能会在默认构造函数中首先初始化,然后在 fb.XXX = YYY 表达式中进行初始化。但是由于作者指出它是 POD 类型,所以这不再是一个问题。 - Dmytro Ovdiienko

9

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

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

我知道这种样式与您不想使用的那种非常接近,但它使常量值更容易替换,并且解释它们(因此不需要编辑注释),如果它们发生更改。

另一件您可以做的事情(因为您很懒)是将构造函数设置为内联,这样您就不必输入太多内容(删除“Foobar ::”并节省在 h 和 cpp 文件之间切换的时间):

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

2
我强烈建议阅读此问题的其他人选择底部代码片段中的样式,如果您只想快速初始化结构并设置一组值。 - kayleeFrye_onDeck

8

您的问题有些困难,因为即使函数:

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

如果你愿意的话,你可以加入类型提升保护。


1
在C++中,你想要的是可以实现的:OP并没有要求帮助防止参数顺序混淆,你提出的模板如何实现这一点?为了简单起见,假设我们有两个参数,它们都是整数。 - max
@max:只有类型不同时(即使它们可以相互转换),它才会防止这种情况,这是 OP 的情况。如果它不能区分类型,那么当然它不起作用,但这是另一个问题。 - Matthieu M.
啊,我明白了。是的,这两个问题是不同的,我猜第二个问题目前在C++中没有一个好的解决方案(但似乎C++ 20正在添加对聚合初始化中C99风格参数名称的支持)。 - max

6
许多编译器的C++前端(包括GCC和clang)都能理解C初始化语法。如果可以的话,就直接使用这种方法。

18
不符合 C++ 标准! - bitmask
5
我知道这不是标准做法,但如果你能使用它,这仍然是初始化结构体最明智的方式。 - Matthias Urlichs
2
你可以通过将错误的构造函数设为私有来保护x和y的类型: private: FooBar(float x, int y) {}; - dmitry_romanov
4
clang(基于LLVM的C++编译器)也支持这种语法。很遗憾它不是标准的一部分。 - nimrodm
我们都知道C初始化器不是C++标准的一部分。但是许多编译器确实理解它,而且问题也没有说明针对哪个编译器,如果有的话。因此,请不要对这个答案进行负评。 - Matthias Urlichs
附言:它们将成为C++20标准的一部分。所以请再等几年吧。;-) - Matthias Urlichs

4

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

2
对于函数式编程来说有些繁琐(例如在函数调用的参数列表中创建对象),但除此之外真的是一个很棒的想法! - bitmask
30
优化器可能会减小它,但我的眼睛并没有。 - Matthieu M.
6
两个单词:“啊...啊!”这种方式比使用“Point pt; pt.x = pt.y = 20;”这样的公共数据好在哪里?或者如果你想要封装,那么这种方式比构造函数更好在哪里? - OldPeculier
3
比起构造函数,这种方式更好,因为你需要查看构造函数的声明来确定参数顺序……是 x、y 还是 y、x,但我展示的方式在调用时显而易见。 - parapura rajkumar
2
如果你想要一个const结构体,或者想要告诉编译器不允许未初始化的结构体,那么这种方法是行不通的。如果你真的想这样做,至少要用inline标记setters! - Matthias Urlichs
显示剩余2条评论

3

我知道这个问题很老了,但是在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.);

这有点不方便,因为你需要针对每个可能的属性数量编写宏,并在宏调用中重复类型和实例名称。而且你不能在返回语句中使用宏,因为断言是在初始化之后执行的。

但它确实解决了你的问题:当你改变结构体时,调用将在编译时失败。

如果你使用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ël Nison
1
@MaëlNison 是的:指定初始化器(自C++20起) - Max Vollmer

3
选项 D: FooBar FooBarMake(int foo, float bar) 这是合法的 C 和 C++ 代码,对于 PODs 可以轻松优化。当然,它没有命名参数,但这就像所有的 C++ 一样。如果你想要命名参数,Objective-C 应该是更好的选择。
选项 E:
FooBar fb;
memset(&fb, 0, sizeof(FooBar));
fb.foo = 4;
fb.bar = 15.5f;

Legal C,合法的C ++。命名参数。


12
在C++中,可以使用FooBar fb = {};代替memset,它会默认初始化所有结构体成员。 - Öö Tiib
@ÖöTiib:不幸的是,这是非法的C语言。 - CB Bailey

2
< p>在C++中,/* B */的写法是没有问题的,而且C++0x会扩展这种语法,使其对C++容器也很有用。我不明白为什么你认为这是不好的风格?

如果你想使用参数名来指示参数,则可以使用boost parameter library,但这可能会让不熟悉它的人感到困惑。

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


7
我认为这种写法很糟糕,因为我认为它没有可维护性。如果明年我想再添加一个成员怎么办?或者如果我改变成员的顺序/类型怎么办?那些初始化这个代码块的地方很可能会(非常有可能)出现错误。 - bitmask
2
@bitmask 但只要您没有使用命名参数,您也必须更新构造函数调用,而且我认为很少有人认为构造函数是不可维护的坏风格。我还认为命名初始化不是C语言,而是C99,而C++绝对不是其超集。 - Christian Rau
2
如果您在一年后向结构体末尾添加另一个成员,则它将在已存在的代码中进行默认初始化。如果您重新排序它们,则必须编辑所有现有的代码,这是无法避免的。 - Öö Tiib
1
@位掩码:第一个例子也会变得“难以维护”。如果在结构体中重命名变量会发生什么?当然,您可以进行替换全部操作,但这可能会意外地重命名不应该重命名的变量。 - Mike Bailey
@altendky 当然,C99就是C。但这与问题无关,因为这个特定的C99功能(命名初始化)不是C++子集的一部分,甚至不是包含在C++11中的C99部分的一部分。但我同意那个评论可能没有表达得那么准确。 - Christian Rau
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接