在C++中初始化结构体

28

作为对这个问题的补充,这里发生了什么:

#include <string>
using namespace std;

struct A {
    string s;
};

int main() {
    A a = {0};
}

很明显,你不能把std::string设置为零。能否有人提供一个解释(请提供对C++标准的引用支持),说明在这里实际上应该发生什么?然后举例解释:

int main() {
    A a = {42};
}

这两者是否都是明确定义的?

对我来说,这又是一个尴尬的问题 - 我总是为我的结构体提供构造函数,所以这个问题从来没有出现过。


1
boost::array类模板也是一个聚合体。因此,您可以使用它进行array<std::string, 2> a = { "foo", "bar" };等操作。此外,我的lazy-construct-array也是一个聚合体:https://dev59.com/5E3Sa4cB1Zd3GeqPtkCH#2662526 - Johannes Schaub - litb
3
隐式转换 + 聚合体 - Steve Guidi
@litb 当我第一次看到boost::array的那个特性时,我有了一种启迪,也就是大脑的性满足感。那些简单而又有意义的东西总是能让我产生这种感觉。 - wilhelmtell
5个回答

29
您的结构体是一个“聚合体”,因此普通的聚合初始化规则适用于它。这个过程在8.5.1中进行了描述。基本上整个8.5.1都是专门针对它的,所以我不认为有必要在这里复制整个内容。总体思路与C语言基本相同,只是根据C++做了调整:您从右侧获取一个初始化程序,从左侧获取一个成员,并使用该初始化程序初始化该成员。根据8.5/12的规定,这将是一种“复制初始化”。
当您执行以下操作时:
A a = { 0 };

你基本上是在使用0进行复制初始化a.s,也就是说对于a.s而言,在语义上等同于

string s = 0;

上述代码之所以能够编译成功,是因为 std::string 可以从 const char * 指针进行转换。(但这是未定义行为,因为在这种情况下 null 指针不是一个有效的参数。)

你的 42 版本也同样不能编译,原因与上述相同。

string s = 42;

将不会编译。 42 不是空指针常量,而且 std::string 没有从 int 类型进行转换的方法。

P.S. 仅供参考:请注意,C++中“聚合类型”(与POD的定义相反)并非递归式的。 std::string 不是聚合类型,但这对于您的 A 并无影响。 A 仍然是一个聚合类型。


§12.6.1也是相关的,如§8.5.1所述。13。 - outis
@outis:我看了12.6.1,但我并没有立刻看出它对8.5中已有内容的补充。每次12.6.1处理聚合初始化时,似乎都在参考8.5 :) - AnT stands with Russia
basic_string(size_type n, charT c, const Allocator a=Allocator())中,看到size_type n没有默认值是很有趣的。原因是在指针和整数上进行重载是一个坏主意。值0(零)严格来说是一个整数,而不是一个指针,因此,除非你显式地进行转换,否则你将无法通过空指针构造指针重载。标准通过要求字符类型来避免这种混淆,如果您在字符串构造时指定了字符串长度。 - wilhelmtell
它指定了聚合类成员何时进行复制初始化、值初始化或者初始化器是非法的。 - outis

8

8.5.1/12 "聚合体" 中指出:

当使用初始化列表中的初始化器来初始化聚合体成员时,所有隐式类型转换(第4条款)都会被考虑。

因此

A a = {0};

将使用NULL char*(正如AndreyTJohannes所指出的)初始化。

A a = {42};

由于没有与std::string构造函数匹配的隐式转换,因此在编译时将会失败。


3

0是一个空指针常量

S.4.9:

空指针常量是一个整数类型的整数常量表达式(5.19),其值为零。

空指针常量可以转换为任何其他指针类型:

S.4.9:

空指针常量可以转换为指针类型;结果是该类型的空指针值。

您提供的关于A的定义被视为聚合体:

S.8.5.1:

聚合体是没有用户声明的构造函数、没有私有或受保护的非静态数据成员、没有基类和没有虚函数的数组或类。

您正在指定初始化程序:

S.8.5.1:

当初始化聚合体时,初始化程序可以包含一个初始化程序子句,该子句由一个花括号括起来的、逗号分隔的初始化程序子句列表组成,用于聚合体的成员。

A包含聚合体类型为std::string的成员,并且初始化程序子句适用于它。

您的聚合体是复制初始化的

当聚合体(无论是类还是数组)包含类类型成员,并由括号括起来的初始化程序列表初始化时,每个这样的成员都被复制初始化。

复制初始化意味着您等价于std::string s = 0std::string s = 42

S.8.5-12

在参数传递、函数返回、抛出异常(15.1)、处理异常(15.3)和花括号括起来的初始化程序列表(8.5.1)中发生的初始化称为复制初始化,相当于形式T x = a;

std::string s = 42将无法编译,因为没有隐式转换,std::string s = 0将编译(因为存在隐式转换),但会导致未定义行为。

std::stringconst char*构造函数未定义为explicit,这意味着您可以这样做:std::string s = 0

只是为了显示实际上正在进行复制初始化,您可以进行以下简单测试:

class mystring
{
public:

  explicit mystring(const char* p){}
};

struct A {
  mystring s;
};


int main()
{
    //Won't compile because no implicit conversion exists from const char*
    //But simply take off explicit above and everything compiles fine.
    A a = {0};
    return 0;
}

2

正如其他人所指出的那样,这段代码“可用”是因为字符串有一个可以接受0作为参数的构造函数。如果我们写成:

#include <map>
using namespace std;

struct A {
    map <int,int> m;
};

int main() {
    A a = {0};
}

然后我们会得到一个编译错误,因为map类没有这样的构造函数。


为什么你的头像在这个回答中没有显示?它被隐藏在社区回答中了吗? - Johannes Schaub - litb
@Johannes 一个谜题!你想在 Meta 上报告它作为一个 bug 吗?还是我来报告? - anon
你的身份不属于你,而属于集体。 - wilhelmtell

1
在21.3.1/9中,标准禁止std::basic_string相关构造函数的char*参数为空指针。这应该会抛出一个std::logic_error异常,但是我还没有看到标准保证违反前置条件会抛出std::logic_error的地方。

1
如果我没记错的话,违反前置条件会保证未定义行为,而不是异常。 - James McNellis
1
@James 在 OS X 10.5.8 上使用 g++ 4.0.1 在构建时会抛出 std::logic_error。19.1.1 表明这就是 logic_error 的作用,但我找不到保证当不变式或前置条件被违反时会发生这种情况的保证。 - wilhelmtell

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