为啥我的编译器无法计算出这种转换,它何时存在?

Posted

技术标签:

【中文标题】为啥我的编译器无法计算出这种转换,它何时存在?【英文标题】:Why can my compiler not figure out this conversion, when does it exist?为什么我的编译器无法计算出这种转换,它何时存在? 【发布时间】:2020-01-16 21:06:12 【问题描述】:

看起来当我创建一个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;

Try it here.

编辑:

作为一种解决方法,做这样的事情 std::initializer_list&lt;A*&gt; alist = b1, b2; 似乎被do_nothing() 接受,但我仍然对这种行为感到好奇。

【问题讨论】:

非常相似/重复:I want a vector of derived class pointers as base class pointers 有趣。但是,std::initializer_list 似乎没有相应的构造函数。我实际上可以通过使用&lt;A*&gt; 声明列表来解决这个问题,但我仍然对为什么会出现这种行为感到好奇。 Brace-init list b1, b2 不会创建 std::initializer_list&lt;B*&gt; 的实例。相反,它用于初始化任何它的初始化器 - 在这种情况下,类型为 std::initializer_list&lt;A*&gt; 的参数 感谢@IgorTandetnik,这是有道理的。我刚刚发现我可以做std::initializer_list&lt;A*&gt; list = b1, b2,它为我解决了这个问题,但我仍然很好奇为什么没有用该语言实现转换。 B * 可以隐式转换为A *,因为B 继承自A。但是,initializer_list&lt;B *&gt; 不能隐式转换为 initializer_list&lt;A *&gt;,因为 initializer_list&lt;B *&gt; 不继承自 initializer_list&lt;A *&gt; 【参考方案1】:

原因是这里的初始化列表

do_nothing(b1, b2);

的类型不同
std::initializer_list<B*> blist = b1, b2;

由于do_nothing 采用std::initializer_list&lt;A*&gt;,函数调用中的大括号初始化列表(do_nothing(b1, b2))用于从函数参数构造std::initializer_list&lt;A*&gt;。这是可行的,因为B* 可以隐式转换为A*。但是,std::initializer_list&lt;B*&gt; 不能隐式转换为 std::initializer_list&lt;A*&gt;,因此会出现编译器错误。

让我们编写一些伪代码来演示发生了什么。首先我们看一下代码的工作部分:

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-listinitializer_list。虽然看起来很相似,但这是两个截然不同的东西。

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);

【讨论】:

【参考方案2】:

我们必须区分std::initializer_list&lt;T&gt; 对象和braced-init-list

Braced-init-lists 用大括号括起来,用逗号分隔,是一个源代码级结构。它们可以在各种上下文中找到,并且它们的元素不需要都是相同的类型。例如

std::pair<int, std::string> = 47, "foo";  // <- braced-init-list

当执行包含 braced-init-list 的语句时,有时会从 braced-init 的元素创建一个 std::initializer_list&lt;T&gt; 对象(对于某些 T) -列表。这些上下文是:

当声明为std::initializer_list&lt;T&gt; 类型的变量有一个braced-init-list 作为其初始化器时;和 当对象声明为auto 类型并从非空braced-init-list 复制初始化时。

如果无法从 braced-init-list 创建 std::initializer_list&lt;T&gt; 对象,则会出现编译错误。例如

std::initializer_list<int> l = 47, "foo";  // error

除了上述创建std::initializer_list 对象的方法外,还有另一种方法:复制现有的。这是一个浅拷贝(它只是使新对象指向与旧对象相同的数组)。当然,副本不会改变类型。


现在回到您的代码。当您尝试使用参数blist 调用do_nothing 函数时,您正在尝试做一些不可能的事情,因为您提供的内容不能用于创建std::initializer_list&lt;A*&gt; 对象。创建此类对象的唯一方法是使用 braced-init-list 或现有的 std::initializer_list&lt;A*&gt; 对象。

但是,将b2, b1 作为参数传递就可以了,因为它是一个braced-init-list。允许隐式转换,从 braced-init-list 的元素到所需的元素类型。

【讨论】:

【参考方案3】:

std::initializer_list 是一个模板。

通常在 C++ 中,some_template&lt;T&gt;some_template&lt;U&gt; 无关,即使 TU 相关。

在调用do_something(b1, b2) 中,编译器实际上创建了std::initializer_list&lt;A*&gt;(在应用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 的类型)

更多可能的解决方案请参考this related question。

【讨论】:

以上是关于为啥我的编译器无法计算出这种转换,它何时存在?的主要内容,如果未能解决你的问题,请参考以下文章

我的 Anagram 程序可以运行吗?如果是,为啥不编译?

为啥在这种重载解析情况下编译器不能告诉更好的转换目标? (协方差)

为啥 Clojure 编译器不会为不正确的类型提示抛出错误?

为啥编译器试图将 char * 转换为 char? [关闭]

C# 到 VB.Net:为啥转换为 VB 时编译失败?

为啥 OpenDDS 无法编译?