c++中为什么push_back({1,2})可以,emplace_back({1,2})会报错?

以下代码:

cpp 复制代码
vector<vector<int>> a;

a.push_back({1,2}); // 可以
a.emplace_back({1,2});// 报错

push_back() 可认为是对入参 {1, 2 } 做了初始化列表的隐式转换?

为什么到了 emplace_back() 中,入参 写 {1, 2} 就不让编译了?是无法隐式转换吗?

为方便表达,我们把 "vector<int>" 取别名为 VecInts :

cpp 复制代码
using IntVec =  std::vector<int>;

IntVec 有个构造函数,以 initializer_list<int> 作为入参,略去有默认值的第二个入参,它的原型大概这样子:

cpp 复制代码
IntVec (initializer_list<int> data);

这个initizlizer_list<T> 的类型模板,是 C++11 引入的,这种类型的数据,如果是字面量,要怎么写呢?答:

cpp 复制代码
{ V1, V2, V3, V4 ... Vn }

n 表示个数,即个数不定,另外还有个限制: 各个 V 都得是 T 类型,也就是 V1...Vn 是同类型的。套到本例,可以这样构造一个 initializer_list<int> 数据,并传给 IntVec 的构造函数:

initialize_list<int> d { 90, 88, 100 };
auto dv = IntVec(d);

等一下! 如果有一个函数叫 "foo(int a)",那么,如果你想把 10 传给它,你会这样写吗?

void foo(int a); // 有个函数

int i = 10; // 先定义个临时变量
foo(i);   // 再把变量作为实参传递

十有八九会这么写:

foo(10); // 多直观,多省事

foo(int) 函数的原型说:"我可以要一个int",而你也恰好有一个整数 ,你就直接给它喽!

IntVec (构造)函数说:"我可以要一个 initialize_list<int> ",而你手上正好有一个 {90, 88, 100},于是,你也就直接给它喽!所以,不用临时的 d 变量,直接这样写也行:

cpp 复制代码
auto dv = IntVec ({99, 89, 60, 78});

由于 IntVec () 是一个类型的构造函数,所以当然也可以这样写:

cpp 复制代码
IntVec dv ({99, 89, 60, 78});

再等一下!那对 ( ) 看起来很多余,所以,也可以这么写:

cpp 复制代码
IntVec dv {99, 89, 60, 78};

插个话:大多数语言,包括C语言和C++自己,在定义并初始化一个变量时,都喜欢用 = ,所以下面这么写也很正确:

IntVec dv = {99, 89, 60, 78};

再再等一下!,既然提到了 C 语言,来看下面的 C 代码:

cpp 复制代码
struct S
{
    int i;
    char c;
    double d;
    char *pc; 
};

void demo()
{
   S s = {99, 'A', 99.5, "abcdefg"};
}

这段代码当然也是一段合法的 C++ 代码。请问,该 C++ 代码中的 {99, 'A', 99.5, "abcdefg"} 是一个 initialize_list<T> 吗?

当然不是:四个元素的类型并不一致。

可是,你就没有内心阴暗地------想看编译器笑话的------坏坏地想到什么吗------我猜你有:

cpp 复制代码
struct XLYA  // 心里阴暗
{
    int a, b, c, d;
};

void demo()
{
    XLYA xlya = {99, 89, 60, 78};
}

现在这个 {99, 89, 60, 78},它是一个 initialize_list<int> 吗?

编译器如果会说话,肯定在嘲笑我们 :当然也不是,很简单的推理过程:人家 IntVec------也就是 std::vector<int> 是因为有一个构造函数用到了 initialize_list<int> 作入参,可你这个 struct XLYA 定义中,从头到尾没出现过 initialize_list<int> ,你们这是在心理阴暗什么?

且慢!很多人说 为了兼容C 让 C++背上的沉重的包袱,其实,在很长时间里,C++ 在兼容 C 这件事上,完全是主动"贴"上去的舔狗样。正常的兼容就是:来自C的代码,我C++能正确编译通过就是,可是曾经的C++对自己的要求是:我新加的语法,如果语义上和C差不多,那用这个新语法写的代码在语法和语义表现上,都要和 C 语言一样。

比如:

cpp 复制代码
class C
{
public:
    int a, b, c, d;
};

void demo()
{
    C c = {1, 2,  3, 4}; 
}

哇,大家都来看,我们对C的兼容,是从根子上兼容的!class 和 public 都是 C 没有,C++才有的,但是,一个所有成员都是 public 的 class ,它和 struct 的作用非常非常地接近,所以,它的对象,也可以使用 { 数据列表 } 来直接初始化哦!

如果仅止于此,说C++是舔狗,确实有些过份了。可是,C++很快将目光看向了下面的代码:

cpp 复制代码
class C
{
    int a, b, c, d;
};

void demo_fail()
{
    C c = {1, 2,  3, 4}; 
}

这段代码编译失败了------理由也很充分: a, b, c, d 都是 私有数据,当然不允许语法上,它们被类外的数据直接地初始化。

如果C++能止步于此,那么,就算是舔狗,它也是一只有自己的人生原则的舔狗,可是,它很快就"灵光一闪"!大叫一声,我们不是还新引入了构造函数吗?

于是,下面的代码,也被舔得非常的丝滑且合法:

cpp 复制代码
class C
{
    int a, b, c, d;
public:
    // 让构造函数带个四个入参
    C (int i, int j, int k, int m)
       : a(i), b(j), c(k), d(m)
    {}
};

void demo_ok()
{
    C c = {1, 2, 3, 4};  // 合法了!
}

读到这里,如果你只是眼前一亮(觉得C++好棒),却没有心里又是一暗(想到可能出现的问题),那么 ,阻碍你学好的C++的最大原因,就是你的单纯。

cpp 复制代码
#include <initializer_list>

class C
{
    int a, b, c, d;
public:
     // 令人眼前一亮的构造
     C (int i, int j, int k, int m) 
         : a(i), b(j), c(k), d(m)
     { std::cout << "1" << std::endl; }

     // 令人心里一暗的构造
     C (std::initializer_list<int> data)
     { std::cout << "2" << std::endl; }    
};

现在,如果我这么写:

cpp 复制代码
C c {99, 89, 60, 78}; 

现在这里的 {99, 89, 60, 78} ,它是一个 initialize_list<int> 吗?这当然不是在问你,而是在问编译器。

编译器说:"别看我,依照语言标准,当有 initializer_list 的重载,就优先走 initializer_list 的版本,所以上面构造 c 时,输出 2 !"foo

"就这么简单吗?"

"嗯,对一个函数的入参是不是 initializer_list 的判断,应该就这么简单吧"。

"你骗人!!"

回到题目: std::vector<IntVec> 的 push_back() 函数虽然有4个重载版本(C++20版本),但全是以下声明的"变体"而已:

cpp 复制代码
void push_back( const IntVec& dv);

根本就没有出现过 initializer_list <int> ,所以,人家题主来问你了,请看代码:

cpp 复制代码
std::vector<IntVec> a;
a.push_back({1,2}); 

你是怎么推导出代码中的 " {1, 2}" 是一个 initializer_list<int> 呢?

"push_back() 确实没有任何以 initializer_list<T> 为入参的重载,但是,它明确说明,它可以要一个 IntVec,而 IntVec 则同样明确地说,它可以要一个 initializer_list<int>,而用户写的 {1,2} 又确实可以是一个 initializer_list<int> ......"

"所以呢?"

"所以我决定,在同一处代码的类型推导,帮,且仅帮一次忙。"

"在哪个环节,帮了个什么忙?"

"push_back() 不能接受 {1, 2}------本来,在这个环节,我就可以报错、可以报怨,但是,我没报。"

"哦,你没报错,那你干嘛了?"

"我决定忍一忍,看看 {1, 2} 能不能一步转成 push_back() 真实想要的东西。 push_back() 真实想要的是一个 IntVec,而 {1, 2} 正好可以一步构造出一个 IntVec ...... 这就是我帮的忙。"

"也就是说,一个函数声明要的 是 A,但用户给的B,但是,B可以一步构造出 A,那么 ,你就会帮这个忙是吗?"

"是的,这是一个通则,不管 B 一步转成 A 用的是 (a) 语言内置类型转换,还是上面演示过的:(b) 出于兼容 C 风格的 { } 初始化,(c) 基于C++的令人'眼前一亮'的,参数个数与类型正好都匹配的,构造函数进行的转换,(d) 或者是基于C++的令人'心里一暗'的带 initializer_list 入参的构造 。 其中的 c 和 d ,因为都是基于构造 函数,所以你们可以通过 explicit 关键字来告诉我:不要帮忙,哪怕只有一步,也不要帮忙!!!"

原来还可以禁止编译器的多事啊?先来试上面提到的 c 的情况:

cpp 复制代码
class C
{
    int a, b, c, d;
public:
    // 让构造函数带个四个入参,但是 加了 explicit
    explicit C (int i, int j, int k, int m)
       : a(i), b(j), c(k), d(m)
    {}
};

void demo_bad()
{
    C c = {1, 2, 3, 4};  // 又不合法了!!!!!
    C c1 {1, 2, 3, 4};  // 也不合法,无关 =  
}

呀!之前我还以为,explicit 只能用在单参数版本的构造或赋值操作符重载上呢,原来用在多入参版本的 构造函数上也有相同的意义 :禁止暗中进行的转换。

既然直接构造出 C对象都不允许,下面的情况当然也会编译失败:

cpp 复制代码
class C
{
    int a, b, c, d;
public:
    // 让构造函数带个四个入参,但是 加了 explicit
    explicit C (int i, int j, int k, int m)
       : a(i), b(j), c(k), d(m)
    {}
};

void my_push_back(const C& c)
{
}

void demo_bad()
{
    // 现在,编译器爱莫能助了,因为 explicit 限制了隐式地将 {1, 2, 3, 4} 转换成 C 对象
    my_push_back({1, 2, 3, 4});  
}

扯了大半天 push_back(T const v),那 emplace_back () 呢?

emplace_back()方法的入参肯定没有任何 initializer_list 的字眼出现。原则上它大概长这样子:

cpp 复制代码
template< typename... Args >
reference emplace_back( Args&&... args );

尽管它也支持 个数不定的入参------似乎有点像 initializer_list ,但人家这个技术点,其实叫"可变个数模板参数",不仅入参个数不定,而且每个入参的类型都不定。

既然入参根本不是 initializer_list,那么,当你像题主那样,传它一个 { 1, 2 } 时:

cpp 复制代码
vector<IntVec> a;
a.emplace_back({1,2});

编译器其实一视同仁,同样没有直接报错(报怨),而是默默地看了一眼 emplace_back 的原型,然后就发现:相比 vector<T>::push_back( const T & v) ,vector<T>::emplace_back( ???? ) 根本没有明说自己要几个参数,以及每个参数应该是什么类型?

------ 这种感觉大概就是谈恋爱时,男生问女生:今晚吃什么?大要能吃多少 时,女生回答了一句 "随便,都可以......"差不多吧,编译器二眼一黑,双手一摊,不干了......

编译器就不能再耐心一些,先不决定 {1,2} 到底是------

  • a 用来兼容某个C风格的结构?
  • b 还是说,将来要用它来调用一个双参数版本的构造或普通函数?
  • c 或者是一个initializer_list <int>?

然后直接 走入 emplace_back 的内心,等到碰上该方法的内部代码真的要使用 入参时,再来决定吗?

首先,这是编译器,不是解释器。

其次,好吧,就让编译器脾气超nice,愿意带着不确定的类型信息,继续往下走......但这就不叫"只帮一次忙了"了,特别的,更不是"在一个位置上,只帮一次忙了",而编译这种事情,并不是让程序(编译器)帮忙越多越好------一口气帮的环节多了,最终结果很有可能连程序员都想不到的结果。

最后,担心"程序员想不到结果 ",那程序员为什么不能去看函数的内部既体实现呢?因为程序员铁定可以看到的,只有函数(包括类方法)的原型,而函数(包括类方法)的实现大有可能是要事先编译成二进制的库文件,你让他怎么看?

最后的最后,万一 emplace_back() 函数内,又调用了一个类似的函数呢?

总而言之,在 emplace_back 入口处,语法上既无法推导出 {1, 2} 是什么,也无法通过只帮一次忙的原则推导出 {1, 2} 应该转换成什么时(比如:是转成一个 struct?还是一个 initializer_list),编译器就必须报错,再往下推导,就"做过了头"了。

人之所以会在这里产生这个问题,多半因为我们在语义上知道 emplace_back 最终就是要为容器插入一个 新元素,而那个元素的类型是确定的。
但在编译器看来 emplace_back 和 foo、bar 没什么两样------也许它需要引入ChatGPT?

更多C++知识点学(闲)习(扯),欢迎到 www.d2school.com

相关推荐
hunandede5 分钟前
FFmpeg 4.3 音视频-多路H265监控录放C++开发十三.2:avpacket中包含多个 NALU如何解析头部分析
c++·ffmpeg·音视频
爱学习的大牛12322 分钟前
通过vmware虚拟机安装和调试编译好的 ReactOS
c++·windows内核
tumu_C2 小时前
C++模板特化实战:在使用开源库boost::geometry::index::rtree时,用特化来让其支持自己的数据类型
c++·开源
杜若南星2 小时前
保研考研机试攻略(满分篇):第二章——满分之路上(1)
数据结构·c++·经验分享·笔记·考研·算法·贪心算法
Neophyte06082 小时前
C++算法练习-day40——617.合并二叉树
开发语言·c++·算法
云空2 小时前
《InsCode AI IDE:编程新时代的引领者》
java·javascript·c++·ide·人工智能·python·php
写bug的小屁孩2 小时前
websocket初始化
服务器·开发语言·网络·c++·websocket·网络协议·qt creator
湖南罗泽南3 小时前
Windows C++ TCP/IP 两台电脑上互相传输字符串数据
c++·windows·tcp/ip
可均可可4 小时前
C++之OpenCV入门到提高005:005 图像操作
c++·图像处理·opencv·图像操作
zyx没烦恼4 小时前
【STL】set,multiset,map,multimap的介绍以及使用
开发语言·c++