在聚合初始化期间从后来的成员表达式中引用早期成员是否定义了行为?
Is it defined behavior to reference an early member from a later member expression during aggregate initialization?
考虑以下几点:
struct mystruct
{
int i;
int j;
};
int main(int argc, char* argv[])
{
mystruct foo{45, foo.i};
std::cout << foo.i << ", " << foo.j << std::endl;
return 0;
}
注意聚合初始化列表中 foo.i
的使用。
g++ 5.2.0
输出
45, 45
这是定义明确的行为吗?此聚合初始化器中的 foo.i
是否始终保证引用正在创建的结构的 i
元素(例如,&foo.i
将引用该内存地址)?
如果我向 mystruct
添加显式构造函数:
mystruct(int i, int j) : i(i), j(j) { }
然后我收到以下警告:
main.cpp:15:20: warning: 'foo.a::i' is used uninitialized in this function [-Wuninitialized]
a foo{45, foo.i};
^
main.cpp:19:34: warning: 'foo.a::i' is used uninitialized in this function [-Wuninitialized]
cout << foo.i << ", " << foo.j << endl;
代码编译,输出为:
45, 0
显然这做了一些不同的事情,我假设这是未定义的行为。是吗?如果是这样,为什么这和没有构造函数时有区别?而且,如何使用用户定义的构造函数获得初始行为(如果它是定义明确的行为)?
来自 [dcl.init.aggr] 8.5.1(2)
When an aggregate is initialized by an initializer list, as specified in 8.5.4, the elements of the initializer list are taken as initializers for the members of the aggregate, in increasing subscript or member order. Each member is copy-initialized from the corresponding initializer-clause.
强调我的
和
Within the initializer-list of a braced-init-list, the initializer-clauses, including any that result from pack expansions (14.5.3), are evaluated in the order in which they appear. That is, every value computation and side effect associated with a given initializer-clause is sequenced before every value computation and side effect associated with any initializer-clause that follows it in the comma-separated list of the initializer-list.
让我相信 class 的每个成员都将按照它们在初始化列表中声明的顺序进行初始化,因为 foo.i
在我们评估它以初始化 j
这应该是定义的行为。
这也得到了 [intro.execution] 1.9(12)
的支持
Accessing an object designated by a volatile glvalue (3.10), modifying an object, calling a library I/O function, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment.
强调我的
在您的第二个示例中,我们没有使用聚合初始化,而是使用列表初始化。 [dcl.init.list] 8.5.4(3) 有
List-initialization of an object or reference of type T is defined as follows:
[...]
- Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated
and the best one is chosen through overload resolution (13.3, 13.3.1.7).
所以现在我们将调用您的构造函数。调用构造函数时 foo.i
尚未初始化,因此我们正在复制未初始化的变量,这是未定义的行为。
how can I get the initial behavior (if it was well-defined behavior) with a user-defined constructor?
引用被构造对象之前初始化参数的参数传参,如下:
mystruct(int i, int& j):i(i),j(j)
你的第二种情况是未定义的行为,你不再使用聚合初始化,它仍然是列表初始化,但在这种情况下你有一个正在调用的用户定义的构造函数。为了将第二个参数传递给您的构造函数,它需要评估 foo.i
但它尚未初始化,因为您尚未进入构造函数,因此您正在生成一个不确定的值和 producing an indeterminate value is undefined behavior.
我们还有 12.7
建设和破坏 [class.cdtor] 部分,其中说:
For an object with a non-trivial constructor, referring to any non-static member or base class of the object
before the constructor begins execution results in undefined behavior [...]
因此,假设第一个示例确实有效,我看不出有什么方法可以使您的第二个示例像第一个示例一样工作。
您的第一个案例似乎应该定义明确,但我在标准草案中找不到似乎明确说明的参考。也许它是缺陷,但否则它将是未定义的行为,因为标准没有定义行为。标准确实告诉我们的是,初始化器是按顺序求值的,副作用是按顺序计算的,从 8.5.4
[dcl.init.list]:[=28= 部分开始]
Within the initializer-list of a braced-init-list, the initializer-clauses, including any that result from pack
expansions (14.5.3), are evaluated in the order in which they appear. That is, every value computation and
side effect associated with a given initializer-clause is sequenced before every value computation and side
effect associated with any initializer-clause that follows it in the comma-separated list of the initializer-list. [...]
但是我们没有明确的文字说明成员在每个元素被求值后被初始化。
MSalters 认为 1.9
部分说:
Accessing an object designated by a volatile glvalue (3.10), modifying an object, calling a library I/O
function, or calling a function that does any of those operations are all side effects, which are changes in the
state of the execution environment. [...]
结合:
[...]very value computation and side effect associated with a given initializer-clause is sequenced before every value computation and side effect associated with any initializer-clause that follows it [...]
足以保证在评估初始化列表的元素时初始化聚合的每个成员。虽然这在 C++11 之前不适用,因为 the order of evaluation of the initializer list was unspecified.
作为参考,如果标准没有强加要求,则行为未在定义未定义行为的 1.3.24
节中定义:
behavior for which this International Standard imposes no requirements
[ Note: Undefined behavior may be expected when this International Standard omits any explicit definition of
behavior or [...]
更新
Johannes Schaub 指出 defect report 1343: Sequencing of non-class initialization and std-discussion threads Is aggregate member copy-initialization associated with the corresponding initializer-clause? and Is copy-initialization of an aggregate member associated with the corresponding initializer-clause? 这些都是相关的。
他们基本上指出第一种情况目前未指定,我会quote Richard Smith:
So the only question is, is the side-effect of initializing s.i
"associated with" the evaluation of the full-expression "5"? I think
the only reasonable assumption is that it is: if 5 were initializing a
member of class type, the constructor call would obviously be part of
the full-expression by the definition in [intro.execution]p10, so it
is natural to assume that the same is true for scalar types.
However, I don't think the standard actually explicitly says this
anywhere.
因此,尽管如多处所示,当前的实现看起来符合我们的预期,但在正式澄清或实现提供保证之前依赖它似乎是不明智的。
C++20 更新
随着 Designated Initialization proposal: P0329 这个问题的答案改变为第一种情况。它包含以下部分:
Add a new paragraph to 11.6.1 [dcl.init.aggr]:
The initializations of the elements of the aggregate are evaluated in the element order. That is,
all value computations and side effects associated with a given element are sequenced before
我们可以看到这反映在 latest draft standard
我的第一个想法是 UB,但您完全处于聚合初始化的情况下。 C++ 11 规范草案 n4296 在 8.5.1 聚合 [dcl.init.aggr] 段落中是明确的:
An aggregate is an array or a class with no user-provided constructors , no private or protected non-static data members, no base classes, and no virtual functions
之后:
When an aggregate is initialized by an initializer list, as specified in 8.5.4, the elements of the initializer list
are taken as initializers for the members of the aggregate, in increasing subscript or member order
(强调我的)
我的理解是mystruct foo{45, foo.i};
先用45初始化foo.i
,然后用foo.i
初始化foo.j
。
无论如何我都不敢在实际代码中使用它,因为即使我相信它是标准定义的,恐怕编译器程序员的想法会有所不同。 ..
考虑以下几点:
struct mystruct
{
int i;
int j;
};
int main(int argc, char* argv[])
{
mystruct foo{45, foo.i};
std::cout << foo.i << ", " << foo.j << std::endl;
return 0;
}
注意聚合初始化列表中 foo.i
的使用。
g++ 5.2.0
输出
45, 45
这是定义明确的行为吗?此聚合初始化器中的 foo.i
是否始终保证引用正在创建的结构的 i
元素(例如,&foo.i
将引用该内存地址)?
如果我向 mystruct
添加显式构造函数:
mystruct(int i, int j) : i(i), j(j) { }
然后我收到以下警告:
main.cpp:15:20: warning: 'foo.a::i' is used uninitialized in this function [-Wuninitialized]
a foo{45, foo.i};
^
main.cpp:19:34: warning: 'foo.a::i' is used uninitialized in this function [-Wuninitialized]
cout << foo.i << ", " << foo.j << endl;
代码编译,输出为:
45, 0
显然这做了一些不同的事情,我假设这是未定义的行为。是吗?如果是这样,为什么这和没有构造函数时有区别?而且,如何使用用户定义的构造函数获得初始行为(如果它是定义明确的行为)?
来自 [dcl.init.aggr] 8.5.1(2)
When an aggregate is initialized by an initializer list, as specified in 8.5.4, the elements of the initializer list are taken as initializers for the members of the aggregate, in increasing subscript or member order. Each member is copy-initialized from the corresponding initializer-clause.
强调我的
和
Within the initializer-list of a braced-init-list, the initializer-clauses, including any that result from pack expansions (14.5.3), are evaluated in the order in which they appear. That is, every value computation and side effect associated with a given initializer-clause is sequenced before every value computation and side effect associated with any initializer-clause that follows it in the comma-separated list of the initializer-list.
让我相信 class 的每个成员都将按照它们在初始化列表中声明的顺序进行初始化,因为 foo.i
在我们评估它以初始化 j
这应该是定义的行为。
这也得到了 [intro.execution] 1.9(12)
的支持Accessing an object designated by a volatile glvalue (3.10), modifying an object, calling a library I/O function, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment.
强调我的
在您的第二个示例中,我们没有使用聚合初始化,而是使用列表初始化。 [dcl.init.list] 8.5.4(3) 有
List-initialization of an object or reference of type T is defined as follows:
[...]
- Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution (13.3, 13.3.1.7).
所以现在我们将调用您的构造函数。调用构造函数时 foo.i
尚未初始化,因此我们正在复制未初始化的变量,这是未定义的行为。
how can I get the initial behavior (if it was well-defined behavior) with a user-defined constructor?
引用被构造对象之前初始化参数的参数传参,如下:
mystruct(int i, int& j):i(i),j(j)
你的第二种情况是未定义的行为,你不再使用聚合初始化,它仍然是列表初始化,但在这种情况下你有一个正在调用的用户定义的构造函数。为了将第二个参数传递给您的构造函数,它需要评估 foo.i
但它尚未初始化,因为您尚未进入构造函数,因此您正在生成一个不确定的值和 producing an indeterminate value is undefined behavior.
我们还有 12.7
建设和破坏 [class.cdtor] 部分,其中说:
For an object with a non-trivial constructor, referring to any non-static member or base class of the object before the constructor begins execution results in undefined behavior [...]
因此,假设第一个示例确实有效,我看不出有什么方法可以使您的第二个示例像第一个示例一样工作。
您的第一个案例似乎应该定义明确,但我在标准草案中找不到似乎明确说明的参考。也许它是缺陷,但否则它将是未定义的行为,因为标准没有定义行为。标准确实告诉我们的是,初始化器是按顺序求值的,副作用是按顺序计算的,从 8.5.4
[dcl.init.list]:[=28= 部分开始]
Within the initializer-list of a braced-init-list, the initializer-clauses, including any that result from pack expansions (14.5.3), are evaluated in the order in which they appear. That is, every value computation and side effect associated with a given initializer-clause is sequenced before every value computation and side effect associated with any initializer-clause that follows it in the comma-separated list of the initializer-list. [...]
但是我们没有明确的文字说明成员在每个元素被求值后被初始化。
MSalters 认为 1.9
部分说:
Accessing an object designated by a volatile glvalue (3.10), modifying an object, calling a library I/O function, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment. [...]
结合:
[...]very value computation and side effect associated with a given initializer-clause is sequenced before every value computation and side effect associated with any initializer-clause that follows it [...]
足以保证在评估初始化列表的元素时初始化聚合的每个成员。虽然这在 C++11 之前不适用,因为 the order of evaluation of the initializer list was unspecified.
作为参考,如果标准没有强加要求,则行为未在定义未定义行为的 1.3.24
节中定义:
behavior for which this International Standard imposes no requirements [ Note: Undefined behavior may be expected when this International Standard omits any explicit definition of behavior or [...]
更新
Johannes Schaub 指出 defect report 1343: Sequencing of non-class initialization and std-discussion threads Is aggregate member copy-initialization associated with the corresponding initializer-clause? and Is copy-initialization of an aggregate member associated with the corresponding initializer-clause? 这些都是相关的。
他们基本上指出第一种情况目前未指定,我会quote Richard Smith:
So the only question is, is the side-effect of initializing s.i "associated with" the evaluation of the full-expression "5"? I think the only reasonable assumption is that it is: if 5 were initializing a member of class type, the constructor call would obviously be part of the full-expression by the definition in [intro.execution]p10, so it is natural to assume that the same is true for scalar types.
However, I don't think the standard actually explicitly says this anywhere.
因此,尽管如多处所示,当前的实现看起来符合我们的预期,但在正式澄清或实现提供保证之前依赖它似乎是不明智的。
C++20 更新
随着 Designated Initialization proposal: P0329 这个问题的答案改变为第一种情况。它包含以下部分:
Add a new paragraph to 11.6.1 [dcl.init.aggr]:
The initializations of the elements of the aggregate are evaluated in the element order. That is, all value computations and side effects associated with a given element are sequenced before
我们可以看到这反映在 latest draft standard
我的第一个想法是 UB,但您完全处于聚合初始化的情况下。 C++ 11 规范草案 n4296 在 8.5.1 聚合 [dcl.init.aggr] 段落中是明确的:
An aggregate is an array or a class with no user-provided constructors , no private or protected non-static data members, no base classes, and no virtual functions
之后:
When an aggregate is initialized by an initializer list, as specified in 8.5.4, the elements of the initializer list are taken as initializers for the members of the aggregate, in increasing subscript or member order
(强调我的)
我的理解是mystruct foo{45, foo.i};
先用45初始化foo.i
,然后用foo.i
初始化foo.j
。
无论如何我都不敢在实际代码中使用它,因为即使我相信它是标准定义的,恐怕编译器程序员的想法会有所不同。 ..