复制初始化形式 '= {}'
Copy initialization of the form '= {}'
鉴于以下情况:
#include <stdio.h>
class X;
class Y
{
public:
Y() { printf(" 1\n"); } // 1
// operator X(); // 2
};
class X
{
public:
X(int) {}
X(const Y& rhs) { printf(" 3\n"); } // 3
X(Y&& rhs) { printf(" 4\n"); } // 4
};
// Y::operator X() { printf(" operator X() - 2\n"); return X{2}; }
int main()
{
Y y{}; // Calls (1)
printf("j\n");
X j{y}; // Calls (3)
printf("k\n");
X k = {y}; // Calls (3)
printf("m\n");
X m = y; // Calls (3)
printf("n\n");
X n(y); // Calls (3)
return 0;
}
到目前为止,还不错。现在,如果我启用转换运算符 Y::operator X()
,我会得到这个;-
X m = y; // Calls (2)
我的理解是,发生这种情况是因为 (2) 比 (3) 'less const' 并且
因此首选。省略了对 X
构造函数的调用
我的问题是,为什么定义 X k = {y}
没有以同样的方式改变它的行为?我知道 = {}
在技术上是 'list copy initialization',但是在没有采用 initializer_list
类型的构造函数的情况下,这不会恢复到 'copy initialization' 行为吗?即 - 与 X m = y
相同
我理解的漏洞在哪里?
Where is the hole in my understanding?
tltldr;没人懂初始化。
tldr;列表初始化更喜欢 std::initializer_list<T>
构造函数,但它不会回退到非列表初始化。它只会退回到考虑构造函数。非列表初始化会考虑转换函数,但回退不会。
所有初始化规则均来自[dcl.init]。所以让我们从第一原则开始吧。
- If the initializer is a (non-parenthesized) braced-init-list or is = braced-init-list, the object or reference is list-initialized.
第一个要点涵盖所有列表初始化。这会跳转 X x{y}
和 X x = {y}
到 [dcl.init.list]。我们会回到那个。另一种情况更容易。让我们看看X x = y
。我们直接向下调用:
- Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in [over.match.copy], and the best one is chosen through overload resolution.
[over.match.copy] 中的候选人是:
- The converting constructors of
T
[in our case, X
] are candidate functions.
- When the type of the initializer expression is a class type “cv
S
”, the non-explicit conversion functions of S
and its base classes are considered.
In both cases, the argument list has one argument, which is the initializer expression.
这给了我们候选人:
X(Y const &); // from the 1st bullet
Y::operator X(); // from the 2nd bullet
第二个相当于有一个 X(Y& )
,因为转换函数不是 cv 限定的。与转换构造函数相比,这使得 cv 限定引用更少,因此它是首选。注意,在 C++17 中没有调用 X(X&& )
。
现在让我们回到列表初始化案例。第一个相关的要点是 [dcl.init.list]/3.6:
Otherwise, if T
is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution ([over.match], [over.match.list]). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed.
这两种情况都将我们带到 [over.match.list],它定义了两相过载解决方案:
- Initially, the candidate functions are the initializer-list constructors ([dcl.init.list]) 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.
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.
候选人是X
的构造者。 X x{y}
和 X x = {y}
之间的唯一区别是,如果后者选择 explicit
构造函数,则初始化格式错误。我们甚至没有任何 explicit
构造函数,所以两者是等价的。因此,我们枚举我们的构造函数:
X(Y const& )
X(X&& )
通过 Y::operator X()
前者是直接引用绑定,是精确匹配。后者需要用户定义的转换。因此,在这种情况下,我们更喜欢X(Y const& )
。
请注意,gcc 7.1 在 C++1z 模式下会出错,因此我已提交 bug 80943。
My question is, why doesn't the definition X k = {y} change its behavior in the same way?
因为,从概念上讲,= { .. }
是一个 初始化,用于自动选择 "best" 方式来初始化目标 从大括号开始,虽然= value
也是一个初始化,但在概念上也是值到不同值的转换。转换是完全对称的:如果将查看源值以查看它是否提供创建目标的方法,并将查看目标以查看它是否提供接受源的方法。
如果您的目标类型是 struct A { int x; }
,那么使用 = { 10 }
将不会尝试将 10
转换为 A
(这会失败)。但它会寻求最好的(在他们眼中)初始化形式,这里相当于聚合初始化。但是,如果 A
不是聚合(添加构造函数),那么它将调用构造函数,在您的情况下,它会发现 Y
无需转换即可轻松接受。源和目标之间没有像使用 = value
形式时的转换那样的对称性。
你对转换函数"less const"的怀疑是完全正确的。如果将转换函数设为 const 成员,那么它将变得不明确。
鉴于以下情况:
#include <stdio.h>
class X;
class Y
{
public:
Y() { printf(" 1\n"); } // 1
// operator X(); // 2
};
class X
{
public:
X(int) {}
X(const Y& rhs) { printf(" 3\n"); } // 3
X(Y&& rhs) { printf(" 4\n"); } // 4
};
// Y::operator X() { printf(" operator X() - 2\n"); return X{2}; }
int main()
{
Y y{}; // Calls (1)
printf("j\n");
X j{y}; // Calls (3)
printf("k\n");
X k = {y}; // Calls (3)
printf("m\n");
X m = y; // Calls (3)
printf("n\n");
X n(y); // Calls (3)
return 0;
}
到目前为止,还不错。现在,如果我启用转换运算符 Y::operator X()
,我会得到这个;-
X m = y; // Calls (2)
我的理解是,发生这种情况是因为 (2) 比 (3) 'less const' 并且
因此首选。省略了对 X
构造函数的调用
我的问题是,为什么定义 X k = {y}
没有以同样的方式改变它的行为?我知道 = {}
在技术上是 'list copy initialization',但是在没有采用 initializer_list
类型的构造函数的情况下,这不会恢复到 'copy initialization' 行为吗?即 - 与 X m = y
我理解的漏洞在哪里?
Where is the hole in my understanding?
tltldr;没人懂初始化。
tldr;列表初始化更喜欢 std::initializer_list<T>
构造函数,但它不会回退到非列表初始化。它只会退回到考虑构造函数。非列表初始化会考虑转换函数,但回退不会。
所有初始化规则均来自[dcl.init]。所以让我们从第一原则开始吧。
- If the initializer is a (non-parenthesized) braced-init-list or is = braced-init-list, the object or reference is list-initialized.
第一个要点涵盖所有列表初始化。这会跳转 X x{y}
和 X x = {y}
到 [dcl.init.list]。我们会回到那个。另一种情况更容易。让我们看看X x = y
。我们直接向下调用:
- Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in [over.match.copy], and the best one is chosen through overload resolution.
[over.match.copy] 中的候选人是:
- The converting constructors of
T
[in our case,X
] are candidate functions.- When the type of the initializer expression is a class type “cv
S
”, the non-explicit conversion functions ofS
and its base classes are considered.In both cases, the argument list has one argument, which is the initializer expression.
这给了我们候选人:
X(Y const &); // from the 1st bullet
Y::operator X(); // from the 2nd bullet
第二个相当于有一个 X(Y& )
,因为转换函数不是 cv 限定的。与转换构造函数相比,这使得 cv 限定引用更少,因此它是首选。注意,在 C++17 中没有调用 X(X&& )
。
现在让我们回到列表初始化案例。第一个相关的要点是 [dcl.init.list]/3.6:
Otherwise, if
T
is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution ([over.match], [over.match.list]). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed.
这两种情况都将我们带到 [over.match.list],它定义了两相过载解决方案:
- Initially, the candidate functions are the initializer-list constructors ([dcl.init.list]) 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.
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.
候选人是X
的构造者。 X x{y}
和 X x = {y}
之间的唯一区别是,如果后者选择 explicit
构造函数,则初始化格式错误。我们甚至没有任何 explicit
构造函数,所以两者是等价的。因此,我们枚举我们的构造函数:
X(Y const& )
X(X&& )
通过Y::operator X()
前者是直接引用绑定,是精确匹配。后者需要用户定义的转换。因此,在这种情况下,我们更喜欢X(Y const& )
。
请注意,gcc 7.1 在 C++1z 模式下会出错,因此我已提交 bug 80943。
My question is, why doesn't the definition X k = {y} change its behavior in the same way?
因为,从概念上讲,= { .. }
是一个 初始化,用于自动选择 "best" 方式来初始化目标 从大括号开始,虽然= value
也是一个初始化,但在概念上也是值到不同值的转换。转换是完全对称的:如果将查看源值以查看它是否提供创建目标的方法,并将查看目标以查看它是否提供接受源的方法。
如果您的目标类型是 struct A { int x; }
,那么使用 = { 10 }
将不会尝试将 10
转换为 A
(这会失败)。但它会寻求最好的(在他们眼中)初始化形式,这里相当于聚合初始化。但是,如果 A
不是聚合(添加构造函数),那么它将调用构造函数,在您的情况下,它会发现 Y
无需转换即可轻松接受。源和目标之间没有像使用 = value
形式时的转换那样的对称性。
你对转换函数"less const"的怀疑是完全正确的。如果将转换函数设为 const 成员,那么它将变得不明确。