C++ 复制构造函数被调用而不是 initializer_list<>
C++ Copy constructor gets called instead of initializer_list<>
基于此代码
struct Foo
{
Foo()
{
cout << "default ctor" << endl;
}
Foo(std::initializer_list<Foo> ilist)
{
cout << "initializer list" << endl;
}
Foo(const Foo& copy)
{
cout << "copy ctor" << endl;
}
};
int main()
{
Foo a;
Foo b(a);
// This calls the copy constructor again!
//Shouldn't this call the initializer_list constructor?
Foo c{b};
_getch();
return 0;
}
输出为:
默认构造函数
复制构造函数
复制构造函数
在第三种情况下,我将 b 放入应该调用 initializer_list<> 构造函数的大括号初始化中。
相反,复制构造函数带头。
你们中有人能告诉我这是如何工作的吗?为什么?
正如 Nicol Bolas 所指出的,此答案的原始版本是不正确的:撰写本文时的 cppreference 错误地记录了在列表初始化中考虑构造函数的顺序。以下是使用标准 n4140 草案中存在的规则的答案,该标准非常接近官方 C++14 标准。
原答案正文仍收录,备案
更新答案
根据 NathanOliver 的评论,gcc 和 clang 在这种情况下产生不同的输出:
g++ -std=c++14 -Wall -pedantic -pthread main.cpp && ./a.out
default ctor
copy ctor
copy ctor
initializer list
clang++ -std=c++14 -Wall -pedantic -pthread main.cpp && ./a.out
default ctor
copy ctor
copy ctor
gcc 是正确的。
n4140 [dcl.init.list]/1
List-initialization is initialization of an object or reference from a braced-init-list.
你在那里使用列表初始化,因为 c
是一个对象,它的列表初始化规则定义在 [dcl.init.list]/3:
[dcl.init.list]/3:
List-initialization of an object or reference of type T is defined as follows:
- If
T
is an aggregate...
- Otherwise, if the initializer list has no elements...
- Otherwise, if
T
is a specialization of std::initializer_list<E>
...
目前正在浏览列表:
Foo
不是聚合。
- 它有一个元素。
Foo
不是 std::initializer_list<E>
的特化。
然后我们点击 [dcl.init.list]/3.4:
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). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed.
现在我们有所进展。 13.3.1.7 也称为 [over.match.list]:
Initialization by list-initialization
When objects of non-aggregate class type T
are list-initialized (8.5.4), overload resolution selects the constructor in two phases:
- Initially, the candidate functions are the initializer-list constructors (8.5.4) of the class
T
and the argument list consists of the initializer list as a single argument.
- If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class
T
and the argument list consists of the elements of the initializer list.
因此,在重载决议的第二阶段,复制构造函数将仅在初始化列表构造函数之后被考虑。此处应使用初始化列表构造函数。
值得注意的是 [over.match.list] 然后继续:
If the initializer list has no elements and T
has a default constructor, the first phase is omitted. In copy-list initialization, if an explicit constructor is chosen, the initialization is ill-formed.
并且在[dcl.init.list]/3.5之后处理单元素列表初始化:
Otherwise, if the initializer list has a single element of type E
and either T
is not a reference type or its referenced type is reference-related to E
, the object or reference is initialized from that element; if a narrowing conversion (see below) is required to convert the element to T
, the program is ill-formed.
这解释了 cppreference 在何处获得了单元素列表初始化的特殊情况,尽管他们将其置于比应有的顺序更高的位置。
原答案
您遇到了列表初始化的一个有趣方面,如果列表满足某些要求,它可能会被视为复制初始化而不是列表初始化。
来自 cppreference:
The effects of list initialization of an object of type T
are:
If T
is a class type and the initializer list has a single element of
the same or derived type (possibly cv-qualified), the object is
initialized from that element (by copy-initialization for
copy-list-initialization, or by direct-initialization for
direct-list-initialization). (since c++14)
Foo c{b}
满足所有这些要求。
让我们在这里检查一下 C++14 规范中关于列表初始化的内容。 [dcl.init.list]3 有一系列要按顺序应用的规则:
3.1 不适用,因为 Foo
不是聚合。
3.2 不适用,因为列表不为空。
3.3 不适用,因为 Foo
不是 initializer_list
.
的特化
3.4 确实适用,因为 Foo
是 class 类型。它说根据 [over.match.list] 考虑具有重载决议的构造函数。该规则说要检查 initializer_list
构造函数 首先 。由于您的类型有一个 initilaizer_list
构造函数,编译器 必须 检查是否可以从给定的值中制造出与这些构造函数之一匹配的 initializer_list
。可以,所以这才叫。
总之,GCC是对的,Clang是错的。
需要注意的是,C++17 工作草案对此没有任何改变。它有一个新的第 3.1 节,其中有针对单值列表的特殊措辞,但 仅适用于聚合 。 Foo
不是聚合,所以不适用。
基于此代码
struct Foo
{
Foo()
{
cout << "default ctor" << endl;
}
Foo(std::initializer_list<Foo> ilist)
{
cout << "initializer list" << endl;
}
Foo(const Foo& copy)
{
cout << "copy ctor" << endl;
}
};
int main()
{
Foo a;
Foo b(a);
// This calls the copy constructor again!
//Shouldn't this call the initializer_list constructor?
Foo c{b};
_getch();
return 0;
}
输出为:
默认构造函数
复制构造函数
复制构造函数
在第三种情况下,我将 b 放入应该调用 initializer_list<> 构造函数的大括号初始化中。
相反,复制构造函数带头。
你们中有人能告诉我这是如何工作的吗?为什么?
正如 Nicol Bolas 所指出的,此答案的原始版本是不正确的:撰写本文时的 cppreference 错误地记录了在列表初始化中考虑构造函数的顺序。以下是使用标准 n4140 草案中存在的规则的答案,该标准非常接近官方 C++14 标准。
原答案正文仍收录,备案
更新答案
根据 NathanOliver 的评论,gcc 和 clang 在这种情况下产生不同的输出:
g++ -std=c++14 -Wall -pedantic -pthread main.cpp && ./a.out
default ctor
copy ctor
copy ctor
initializer list
clang++ -std=c++14 -Wall -pedantic -pthread main.cpp && ./a.out
default ctor
copy ctor
copy ctor
gcc 是正确的。
n4140 [dcl.init.list]/1
List-initialization is initialization of an object or reference from a braced-init-list.
你在那里使用列表初始化,因为 c
是一个对象,它的列表初始化规则定义在 [dcl.init.list]/3:
[dcl.init.list]/3:
List-initialization of an object or reference of type T is defined as follows:
- If
T
is an aggregate...- Otherwise, if the initializer list has no elements...
- Otherwise, if
T
is a specialization ofstd::initializer_list<E>
...
目前正在浏览列表:
Foo
不是聚合。- 它有一个元素。
Foo
不是std::initializer_list<E>
的特化。
然后我们点击 [dcl.init.list]/3.4:
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). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed.
现在我们有所进展。 13.3.1.7 也称为 [over.match.list]:
Initialization by list-initialization
When objects of non-aggregate class typeT
are list-initialized (8.5.4), overload resolution selects the constructor in two phases:
- Initially, the candidate functions are the initializer-list constructors (8.5.4) of the class
T
and the argument list consists of the initializer list as a single argument.- If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class
T
and the argument list consists of the elements of the initializer list.
因此,在重载决议的第二阶段,复制构造函数将仅在初始化列表构造函数之后被考虑。此处应使用初始化列表构造函数。
值得注意的是 [over.match.list] 然后继续:
If the initializer list has no elements and
T
has a default constructor, the first phase is omitted. In copy-list initialization, if an explicit constructor is chosen, the initialization is ill-formed.
并且在[dcl.init.list]/3.5之后处理单元素列表初始化:
Otherwise, if the initializer list has a single element of type
E
and eitherT
is not a reference type or its referenced type is reference-related toE
, the object or reference is initialized from that element; if a narrowing conversion (see below) is required to convert the element toT
, the program is ill-formed.
这解释了 cppreference 在何处获得了单元素列表初始化的特殊情况,尽管他们将其置于比应有的顺序更高的位置。
原答案
您遇到了列表初始化的一个有趣方面,如果列表满足某些要求,它可能会被视为复制初始化而不是列表初始化。
来自 cppreference:
The effects of list initialization of an object of type
T
are:If
T
is a class type and the initializer list has a single element of the same or derived type (possibly cv-qualified), the object is initialized from that element (by copy-initialization for copy-list-initialization, or by direct-initialization for direct-list-initialization). (since c++14)
Foo c{b}
满足所有这些要求。
让我们在这里检查一下 C++14 规范中关于列表初始化的内容。 [dcl.init.list]3 有一系列要按顺序应用的规则:
3.1 不适用,因为 Foo
不是聚合。
3.2 不适用,因为列表不为空。
3.3 不适用,因为 Foo
不是 initializer_list
.
3.4 确实适用,因为 Foo
是 class 类型。它说根据 [over.match.list] 考虑具有重载决议的构造函数。该规则说要检查 initializer_list
构造函数 首先 。由于您的类型有一个 initilaizer_list
构造函数,编译器 必须 检查是否可以从给定的值中制造出与这些构造函数之一匹配的 initializer_list
。可以,所以这才叫。
总之,GCC是对的,Clang是错的。
需要注意的是,C++17 工作草案对此没有任何改变。它有一个新的第 3.1 节,其中有针对单值列表的特殊措辞,但 仅适用于聚合 。 Foo
不是聚合,所以不适用。