从C++ 14到C++ 17:理解聚合初始化是如何工作的

一、引言

将编译器升级到C++ 17,某些看起来合理的代码停止了编译。这段代码没有使用任何在C++ 17中删除的过时特性,如std::auto_ptrstd::bind1st,但它仍然停止编译。理解这个编译错误将能更好地理解C++ 17的一个新特性:扩展聚合初始化

二、C++ 14中的代码

一个示例代码:

template<typename Derived>
struct Base
{
private:
    Base(){};
    friend Derived;
};

struct Derived : Base<Derived>
{
};

int main()
{
    Derived d{};
}

这段代码是与CRTP(Curiously Recurring Template Pattern)相关的经典技巧,以避免将错误的类传递给CRTP基类。在C++ 14中,上面的代码可以编译,但是稍微修改一下,其中CRTP派生类不将自身作为模板形参传递给基类,即使在C++ 14中也无法编译:

template<typename Derived>
struct Base
{
private:
    Base(){};
    friend Derived;
};

struct X{};

struct Derived : Base<X> // passing the wrong class here
{
};

int main()
{
    Derived d{};
}

当试图构造Derived时,它需要调用基类base的构造函数,但后者是私有的,并且只与模板形参为friend。模板参数必须是Derived才能编译代码。

在c++ 14中,第一个版本编译得很好。下面是C++ 14中第二种情况的编译错误:

<source>: In function 'int main()':
<source>:17:15: error: use of deleted function 'Derived::Derived()'
   17 |     Derived d{};
      |               ^
<source>:11:8: note: 'Derived::Derived()' is implicitly deleted because the default definition would be ill-formed:
   11 | struct Derived : Base<X>
      |        ^~~~~~~
<source>:11:8: error: 'Base<Derived>::Base() [with Derived = X]' is private within this context
<source>:5:5: note: declared private here
    5 |     Base(){};
      |     ^~~~
Compiler returned: 1

三、C++ 17中的代码

继续看一下C++ 14中编译的第一个正确版本:

template<typename Derived>
struct Base
{
private:
    Base(){};
    friend Derived;
};

struct Derived : Base<Derived>
{
};

int main()
{
    Derived d{};
}

如果尝试用C++ 17编译它,会得到以下错误:

<source>: In function 'int main()':
<source>:15:15: error: 'Base<Derived>::Base() [with Derived = Derived]' is private within this context
   15 |     Derived d{};
      |               ^
<source>:5:5: note: declared private here
    5 |     Base(){};
      |     ^~~~

Base仍然是Derivefriend,为什么编译器不会接受构造一个Derived对象?

四、扩展聚合初始化

好,让我们看看这里发生了什么。

c++ 17带来的特性之一是扩展了聚合初始化。

聚合初始化是指调用点通过初始化其成员而不使用显式定义的构造函数来构造对象。示例:

struct X
{
    int a;
    int b;
    int c;
};

然后可以用下面的方法构造X

X x{1, 2, 3};

调用时用1、2和3初始化a、b和c,不需要x的任何构造函数。这是C++ 11开始允许的。

但是,实现这一特性的规则非常严格:类不能有私有成员、基类、虚函数和许多其他东西。

在C++ 17中,其中一条规则得到了放宽:即使类有基类,也可以执行聚合初始化。不过,调用时必须初始化基类。示例:

struct X
{
    int a;
    int b;
    int c;
};

struct Y : X
{
    int d;
};

Y继承自X。在C++ 14中,这使Y无法进行聚合初始化。但是在c++ 17中,可以这样构造一个Y:

Y y{1, 2, 3, 4};
// or
Y y{ {1, 2, 3}, 4};

两种语法分别将a、b、c和d初始化为1、2、3和4。

也可以这样写:

Y y{ {}, 4 };

这将a, b和c初始化为0,d初始化为4。

但是,要注意,这并不等同于这个:

Y y{4};

因为这将a(而不是d)初始化为4,将b, c和d初始化为0。也可以在X中指定部分属性:

Y y{ {1}, 4};

这将a初始化为1,b和c初始化为0,d初始化为4。

现在已经熟悉了扩展聚合初始化,让我们回到初始代码。

五、为什么代码停止编译?

下面的代码在C++ 14中编译良好,在C++ 17中停止编译:

template<typename Derived>
struct Base
{
private:
    Base(){};
    friend Derived;
};

struct Derived : Base<Derived>
{
};

int main()
{
    Derived d{};
}

注意到调用Derived构造的大括号了吗?在C++ 17中,它们触发聚合初始化,并尝试实例化具有私有构造函数的Base。这就是它停止编译的原因。

构造函数的调用位置是构造基类,而不是构造函数本身。如果修改基类,使其与构造函数的调用位置为friend,则代码在C++ 17中也可以很好地编译:

template<typename Derived>
struct Base
{
private:
    Base(){};
    friend int main(); // this makes the code compile
};

struct Derived : Base<Derived>
{
};

int main()
{
    Derived d{};
}

当然,肯定不打算这样写代码,每个调用点都有一个friend是不合理的!这个更改只是为了说明调用点直接调用基类的构造函数这一事实。

要修复代码,可以……去掉括号(哈哈哈哈):

template<typename Derived>
struct Base
{
private:
    Base(){};
    friend Derived;
};

struct Derived : Base<Derived>
{
};

int main()
{
    Derived d;
}

它又可以编译了。

注意,这时已不再从值初始化中获益了。如果Derivedclass包含数据成员,需要确保在显式声明的构造函数中或在类中声明这些成员时初始化它们。

这个例子让我们更好地理解聚合初始化是如何工作的,以及它在C++ 17中是如何变化的。删除两个字符能教会我们多少东西,是不是非常有趣!

六、总结

本文详细介绍了C++ 17中的扩展聚合初始化特性,该特性使得初始化聚合类型的对象变得更加简洁和灵活。通过对比C++ 14中的代码,发现在C++ 17中引入的扩展聚合初始化语法可以大大简化代码,并提供了更好的可读性和可维护性。也指出了一些在C++ 17中代码停止编译的情况,并解释了其中的原因。通过深入理解扩展聚合初始化的语法和语义,可以在自己的项目中充分利用这一特性,提升代码的效率和可靠性。

在这里插入图片描述

  • 21
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lion Long

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值