C++ 17中的扩展聚合初始化
- 一、引言
- 二、C++ 14中的代码
- 三、C++ 17中的代码
- 四、扩展聚合初始化
- 五、为什么代码停止编译?
- 六、总结
一、引言
将编译器升级到C++ 17,某些看起来合理的代码停止了编译。这段代码没有使用任何在C++ 17中删除的过时特性,如std::auto_ptr
或std::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 here5 | 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 context15 | Derived d{};| ^
<source>:5:5: note: declared private here5 | Base(){};| ^~~~
Base
仍然是Derive
的friend
,为什么编译器不会接受构造一个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;
}
它又可以编译了。
注意,这时已不再从值初始化中获益了。如果Derived
或class
包含数据成员,需要确保在显式声明的构造函数中或在类中声明这些成员时初始化它们。
这个例子让我们更好地理解聚合初始化是如何工作的,以及它在C++ 17中是如何变化的。删除两个字符能教会我们多少东西,是不是非常有趣!
六、总结
本文详细介绍了C++ 17中的扩展聚合初始化特性,该特性使得初始化聚合类型的对象变得更加简洁和灵活。通过对比C++ 14中的代码,发现在C++ 17中引入的扩展聚合初始化语法可以大大简化代码,并提供了更好的可读性和可维护性。也指出了一些在C++ 17中代码停止编译的情况,并解释了其中的原因。通过深入理解扩展聚合初始化的语法和语义,可以在自己的项目中充分利用这一特性,提升代码的效率和可靠性。