为什么我的编译器无法计算出这种转换,它何时存在?
Why can my compiler not figure out this conversion, when does it exist?
当我创建一个 std::initializer_list<B*>
其中 class B
派生自 class A
并将其传递给接受 std::initializer_list<A*>
的函数时,编译器会感到困惑.但是,如果我使用大括号初始值设定项就地创建 std::initializer_list<B*>
(我假设它是一个临时的 std::initializer_list
),它就可以很好地转换它。
具体来说,它似乎无法将 std::initializer_list<B*>
转换为 std::initializer_list<A*>
,尽管某些转换显然存在,正如此处的一个工作函数调用所证明的那样。
对此行为的解释是什么?
#include <initializer_list>
#include <iostream>
class A {
public:
A() {}
};
class B : public A {
public:
B() {}
};
using namespace std;
void do_nothing(std::initializer_list<A*> l) {
}
int main() {
B* b1;
B* b2;
std::initializer_list<B*> blist = {b1, b2};
//error, note: candidate function not viable: no known conversion from
//'initializer_list<B *>' to 'initializer_list<A *>' for 1st argument
//do_nothing(blist);
//Totally fine, handles conversion.
do_nothing({b1, b2});
return 0;
}
编辑:
作为解决方法,做这样的事情
std::initializer_list<A*> alist = {b1, b2};
do_nothing()
似乎被接受了,但我仍然对这种行为感到好奇。
原因是这里的初始化列表
do_nothing({b1, b2});
与
的类型不同
std::initializer_list<B*> blist = {b1, b2};
由于 do_nothing
采用 std::initializer_list<A*>
,函数调用 (do_nothing({b1, b2})
) 中的花括号初始化列表用于从函数参数构造 std::initializer_list<A*>
。这是有效的,因为 B*
可以隐式转换为 A*
。但是,std::initializer_list<B*>
不能隐式转换为 std::initializer_list<A*>
,因此会出现编译器错误。
让我们编写一些伪代码来演示会发生什么。首先我们看一下代码的工作部分:
do_nothing({b1, b2}); // call the function with a braced-init-list
// pseudo code starts here
do_nothing({b1, b2}): // we enter the function, here comes our braced-init-list
std::initializer_list<A*> l {b1, b2}; // this is our function parameter that gets initialized with whatever is in that braced-init-list
... // run the actual function body
现在那个不起作用:
std::initializer_list<B*> blist = {b1, b2}; // creates an actual initializer_list
do_nothing(blist); // call the function with the initializer_list, NOT a braced-init-list
// pseudo code starts here
do_nothing(blist): // we enter the function, here comes our initializer_list
std::initializer_list<A*> l = blist; // now we try to convert an initializer_list<B*> to an initializer_list<A*> which simply isn't possible
... // we get a compiler error saying we can't convert between initializer_list<B*> and initializer_list<A*>
注意术语 braced-init-list 和 initializer_list。虽然看起来很相似,但这是两个截然不同的东西。
A braced-init-list 是一对花括号,中间有值,像这样:
{ 1, 2, 3, 4 }
或者这个:
{ 1, 3.14, "different types" }
它是一种用于初始化的特殊构造,在 C++ 语言中有自己的规则。
另一方面,std::initializer_list
只是一个类型(实际上是一个模板,但我们在这里忽略这一事实,因为它并不重要)。您可以从该类型创建一个对象(就像您对 blist
所做的那样)并 初始化 该对象。因为 braced-init-list 是一种初始化形式,我们可以在 std::initializer_list
:
上使用它
std::initializer_list<int> my_list = { 1, 2, 3, 4 };
因为 C++ 有一个特殊的规则,允许我们用 braced-init-list 初始化每个函数参数,do_nothing({b1, b2});
编译。这也适用于多个参数:
void do_something(std::vector<int> vec, std::tuple<int, std::string, std::string> tup)
{
// ...
}
do_something({1, 2, 3, 4}, {10, "first", "and 2nd string"});
或嵌套初始化:
void do_something(std::tuple<std::tuple<int, std::string>, std::tuple<int, int, int>, double> tup)
{
// ...
}
do_something({{1, "text"}, {2, 3, 4}, 3.14});
std::initializer_list
是模板。
通常在 C++ 中,some_template<T>
与 some_template<U>
无关,即使 T
与 U
相关。
在调用 do_something({b1, b2})
中,编译器实际上创建了 std::initializer_list<A*>
(在应用 list-initialization 规则之后)。
您可以模板化 do_nothing
以采用不同的类型:
template<typename T, typename = std::enable_if_t<std::is_convertible<T, A>::value>>
void do_nothing(std::initializer_list<T*> l) {
}
(可选的 SFINAE 部分将 T
限制为可转换为 A
的类型)
有关更多可能的解决方案,请参阅 。
我们必须区分 std::initializer_list<T>
对象和 braced-init-list.
Braced-init-list由大括号括起来,用逗号分隔,并且是源代码级别的构造。它们可以在各种上下文中找到,并且它们的元素不需要都是相同的类型。例如
std::pair<int, std::string> = {47, "foo"}; // <- braced-init-list
当执行包含 braced-init-list 的语句时,有时会创建一个 std::initializer_list<T>
对象(对于某些 T
) braced-init-list 的元素。这些上下文是:
- 当声明类型为
std::initializer_list<T>
的变量具有 braced-init-list 作为其初始值设定项时;和
- 当对象声明为
auto
类型并从非空 braced-init-list. 复制初始化时
如果无法从 braced-init-list 创建 std::initializer_list<T>
对象,则会发生编译错误。例如
std::initializer_list<int> l = {47, "foo"}; // error
除了上述创建 std::initializer_list
对象的方法外,还有一种方法:复制现有对象。这是一个浅拷贝(它只是使新对象指向与旧对象相同的数组)。复制一份,当然不会改变类型。
现在回到你的代码。当您尝试使用参数 blist
调用 do_nothing
函数时,您正在尝试做一些不可能的事情,因为您提供的不是可用于创建 std::initializer_list<A*>
对象的东西。创建此类对象的唯一方法是从 braced-init-list 或现有的 std::initializer_list<A*>
对象。
然而,将 {b2, b1}
作为参数传递就可以了,因为它是一个 braced-init-list。允许隐式转换,从 braced-init-list 的元素到所需的元素类型。
当我创建一个 std::initializer_list<B*>
其中 class B
派生自 class A
并将其传递给接受 std::initializer_list<A*>
的函数时,编译器会感到困惑.但是,如果我使用大括号初始值设定项就地创建 std::initializer_list<B*>
(我假设它是一个临时的 std::initializer_list
),它就可以很好地转换它。
具体来说,它似乎无法将 std::initializer_list<B*>
转换为 std::initializer_list<A*>
,尽管某些转换显然存在,正如此处的一个工作函数调用所证明的那样。
对此行为的解释是什么?
#include <initializer_list>
#include <iostream>
class A {
public:
A() {}
};
class B : public A {
public:
B() {}
};
using namespace std;
void do_nothing(std::initializer_list<A*> l) {
}
int main() {
B* b1;
B* b2;
std::initializer_list<B*> blist = {b1, b2};
//error, note: candidate function not viable: no known conversion from
//'initializer_list<B *>' to 'initializer_list<A *>' for 1st argument
//do_nothing(blist);
//Totally fine, handles conversion.
do_nothing({b1, b2});
return 0;
}
编辑:
作为解决方法,做这样的事情
std::initializer_list<A*> alist = {b1, b2};
do_nothing()
似乎被接受了,但我仍然对这种行为感到好奇。
原因是这里的初始化列表
do_nothing({b1, b2});
与
的类型不同std::initializer_list<B*> blist = {b1, b2};
由于 do_nothing
采用 std::initializer_list<A*>
,函数调用 (do_nothing({b1, b2})
) 中的花括号初始化列表用于从函数参数构造 std::initializer_list<A*>
。这是有效的,因为 B*
可以隐式转换为 A*
。但是,std::initializer_list<B*>
不能隐式转换为 std::initializer_list<A*>
,因此会出现编译器错误。
让我们编写一些伪代码来演示会发生什么。首先我们看一下代码的工作部分:
do_nothing({b1, b2}); // call the function with a braced-init-list
// pseudo code starts here
do_nothing({b1, b2}): // we enter the function, here comes our braced-init-list
std::initializer_list<A*> l {b1, b2}; // this is our function parameter that gets initialized with whatever is in that braced-init-list
... // run the actual function body
现在那个不起作用:
std::initializer_list<B*> blist = {b1, b2}; // creates an actual initializer_list
do_nothing(blist); // call the function with the initializer_list, NOT a braced-init-list
// pseudo code starts here
do_nothing(blist): // we enter the function, here comes our initializer_list
std::initializer_list<A*> l = blist; // now we try to convert an initializer_list<B*> to an initializer_list<A*> which simply isn't possible
... // we get a compiler error saying we can't convert between initializer_list<B*> and initializer_list<A*>
注意术语 braced-init-list 和 initializer_list。虽然看起来很相似,但这是两个截然不同的东西。
A braced-init-list 是一对花括号,中间有值,像这样:
{ 1, 2, 3, 4 }
或者这个:
{ 1, 3.14, "different types" }
它是一种用于初始化的特殊构造,在 C++ 语言中有自己的规则。
另一方面,std::initializer_list
只是一个类型(实际上是一个模板,但我们在这里忽略这一事实,因为它并不重要)。您可以从该类型创建一个对象(就像您对 blist
所做的那样)并 初始化 该对象。因为 braced-init-list 是一种初始化形式,我们可以在 std::initializer_list
:
std::initializer_list<int> my_list = { 1, 2, 3, 4 };
因为 C++ 有一个特殊的规则,允许我们用 braced-init-list 初始化每个函数参数,do_nothing({b1, b2});
编译。这也适用于多个参数:
void do_something(std::vector<int> vec, std::tuple<int, std::string, std::string> tup)
{
// ...
}
do_something({1, 2, 3, 4}, {10, "first", "and 2nd string"});
或嵌套初始化:
void do_something(std::tuple<std::tuple<int, std::string>, std::tuple<int, int, int>, double> tup)
{
// ...
}
do_something({{1, "text"}, {2, 3, 4}, 3.14});
std::initializer_list
是模板。
通常在 C++ 中,some_template<T>
与 some_template<U>
无关,即使 T
与 U
相关。
在调用 do_something({b1, b2})
中,编译器实际上创建了 std::initializer_list<A*>
(在应用 list-initialization 规则之后)。
您可以模板化 do_nothing
以采用不同的类型:
template<typename T, typename = std::enable_if_t<std::is_convertible<T, A>::value>>
void do_nothing(std::initializer_list<T*> l) {
}
(可选的 SFINAE 部分将 T
限制为可转换为 A
的类型)
有关更多可能的解决方案,请参阅
我们必须区分 std::initializer_list<T>
对象和 braced-init-list.
Braced-init-list由大括号括起来,用逗号分隔,并且是源代码级别的构造。它们可以在各种上下文中找到,并且它们的元素不需要都是相同的类型。例如
std::pair<int, std::string> = {47, "foo"}; // <- braced-init-list
当执行包含 braced-init-list 的语句时,有时会创建一个 std::initializer_list<T>
对象(对于某些 T
) braced-init-list 的元素。这些上下文是:
- 当声明类型为
std::initializer_list<T>
的变量具有 braced-init-list 作为其初始值设定项时;和 - 当对象声明为
auto
类型并从非空 braced-init-list. 复制初始化时
如果无法从 braced-init-list 创建 std::initializer_list<T>
对象,则会发生编译错误。例如
std::initializer_list<int> l = {47, "foo"}; // error
除了上述创建 std::initializer_list
对象的方法外,还有一种方法:复制现有对象。这是一个浅拷贝(它只是使新对象指向与旧对象相同的数组)。复制一份,当然不会改变类型。
现在回到你的代码。当您尝试使用参数 blist
调用 do_nothing
函数时,您正在尝试做一些不可能的事情,因为您提供的不是可用于创建 std::initializer_list<A*>
对象的东西。创建此类对象的唯一方法是从 braced-init-list 或现有的 std::initializer_list<A*>
对象。
然而,将 {b2, b1}
作为参数传递就可以了,因为它是一个 braced-init-list。允许隐式转换,从 braced-init-list 的元素到所需的元素类型。