C++17之聚合类扩展
在c++中初始化对象的一种方法是聚合初始化,它允许使用花括号从多个值初始化:
struct Data
{
std::string name;
double value;
};
Data x{"test1", 6.778};
在c++ 17中聚合可以有基类,所以对于从其他类/结构派生的结构,允许初始化列表:
struct MoreData : Data
{
bool done;
};
MoreData y{{"test1", 6.778}, false};
如您所见,聚合初始化现在支持嵌套大括号,以便将值传递给基类的派生成员。
1. 扩展聚合初始化的意图
如果没有这个特性,则从另一个禁用的聚合初始化派生结构,因此必须定义一个构造函数:
struct Cpp14Data : Data
{
bool done;
Cpp14Data (const std::string& s, double d, bool b)
Data{s,d}, done{b}
{
}
};
Cpp14Data y{"test1", 6.778, false};
C++17提供了这个功能,语法略有不同,它很方便的将值传递到基类,如下:
MoreData y{{"test1", 6.778}, false};
2. 使用扩展聚合初始化
一个典型的应用是子类能够使用列表初始化父类的C风格成员变量,子类可以添加额外的数据成员或操作。例如:
struct Data
{
const char* name;
double value;
};
struct PData : Data
{
bool critical;
void print() const
{
std::cout << '[' << name << ',' << value << "]\n";
}
};
PData y{{"test1", 6.778}, false};
y.print();
这里,内嵌括号中的{"test1", 6.778}参数被传递给基类成员变量。
注意,您可以跳过初始值。在这种情况下,元素初始化为零(调用默认构造函数或使用0、false或nullptr初始化基本数据类型)。
例如:
PData a{}; // zero-initialize all elements
PData b{{"msg"}}; // 等同于 {{"msg",0.0},false}
PData c{{}, true}; // 等同于 {{nullptr,0.0},true}
PData d; //基本类型的值是未指定的
注意在这里使用空花括号和完全不使用花括号的区别:
A. a zero-initializes all members,C风格字符串默认地为空指针(nullptr),double值初始化为0.0,bool标志初始化为false。
B. 变量d的定义所有其他成员都没有初始化,并且具有未指定的值。
在visual studio 2019上测试结果如下:
在这里我们把变量Data中的name类型修改为std::string,代码如下:
例1:
#include <iostream>
#include <string>
struct Data
{
std::string name;
double value;
};
struct PData : Data
{
bool critical;
void print() const
{
std::cout << '[' << name << ',' << value << "]\n";
}
};
int main()
{
PData a{}; // zero-initialize all elements
PData b{ {"msg"} }; // same as {{"msg",0.0},false}
PData c{ {}, true }; // same as {{nullptr,0.0},true}
PData d; // values of fundamental types are unspecified
return 0;
}
此时变量a,b都会调用默认构造函数进行初始化。
结果如下:
还可以从非聚合类派生聚合。例如:
struct MyString : std::string
{
void print() const
{
if (empty())
{
std::cout << "<undefined>\n";
}
else
{
std::cout << c_str() << '\n';
}
}
};
MyString y{{"test1"}};
甚至可以从多个基类和/或聚合派生聚合:
template<typename T>
struct D : std::string, std::complex<T>
{
std::string data;
};
然后你可以使用和初始化如下:
D<float> s{{"hello"}, {4.5,6.7}, "world"}; // OK since C++17
std::cout << s.data; // outputs: ”world”
std::cout << static_cast<std::string>(s); // outputs: ”hello”
std::cout << static_cast<std::complex<float>>(s); // outputs: (4.5,6.7)
在这里内部初始化器列表按照基类声明的顺序传递给基类。这个新特性还有助于用很少的代码定义重载lambdas(后续的文章会介绍)。
详细代码如下:
例2:
#include <iostream>
#include <string>
#include <complex>
template<typename T>
struct D : std::string, std::complex<T>
{
std::string data;
};
int main(void)
{
D<float> s{ {"hello"}, {4.5,6.7}, "world" }; // OK since C++17
std::cout << s.data << std::endl; // outputs: ”world”
std::cout << static_cast<std::string>(s) << std::endl;; // outputs: ”hello”
std::cout << static_cast<std::complex<float>>(s) << std::endl; // outputs: (4.5,6.7)
return 0;
}
结果如下:
3. 聚合的定义
总而言之,由于c++ 17将聚合定义为:
a. 数组;
b. 或类类型(类、结构或联合):
-- 没有用户定义的构造上述;
--没有通过using声明的继承构造函数;
--没有private或者protect的non-static数据成员;
--没有virtual函数;
--没有virtual或者private或者protected的基类。
为了能够使用聚合,还需要在初始化期间不使用私有或受保护基类的成员或构造函数。
c++ 17还引入了一个新的类型trait is_aggregate<>来测试一个类型是否是一个聚合:
template<typename T>
struct D : std::string, std::complex<T>
{
std::string data;
};
D<float> s{{"hello"}, {4.5,6.7}, "world"}; // OK since C++17
std::cout << std::is_aggregate<decltype(s)>::value; // outputs: 1 (true)
例 3:
#include <iostream>
#include <string>
#include <complex>
template<typename T>
struct D : std::string, std::complex<T>
{
std::string data;
};
int main(void)
{
D<float> s{ {"hello"}, {4.5,6.7}, "world" }; // OK since C++17
const auto result = std::is_aggregate<decltype(s)>::value;
std::cout << result << std::endl;
return 0;
}
结果如下:
4. 向前不兼容
注意下面的例子中不会兼容c++14的语法。
例4:
#include<iostream>
struct Derived;
struct Base
{
friend struct Derived;
private:
Base()
{
std::cout << "Base constructor." << std::endl;
}
};
struct Derived : Base
{
};
int main()
{
Derived d1{}; // ERROR since C++17
Derived d2; // still OK (but might not initialize)
return 0;
}
C++17编译会报如下错误:
C++14编译输出结果如下;
Base constructor.
Base constructor.
在c++ 17之前,Derived不是一个聚合。因此,Derived d1 {}调用派生类的编译期自动生成的默认构造函数,该构造函数在默认情况下调用基类基的默认构造函数。虽然基类的默认构造函数是私有的,但是可以通过派生类的默认构造函数调用它,因为派生类被定义为friend类。
自从c++ 17起,Derived在这个示例中是一个聚合类,根本没有一个隐式默认构造函数(没有通过using声明的继承构造函数)。因此,初始化是一个聚合初始化,它不允许调用基类的私有构造函数。是否是基类的friend类并不重要。
4 用户提供的构造函数和聚合类
从C++11到C++17,用户提供的构造函数是不允许的。
在前面我们提到没有用户提供的构造函数是聚合类型的条件之一,但是请注意,用户提供的构造函数和用户声明的构造函数是有区别的,比如:
#include <iostream>
struct X
{
X() = default;
};
struct Y
{
Y() = delete;
};
int main()
{
std::cout << std::boolalpha << "std::is_aggregate_v<X> : " << std::is_aggregate_v<X> << std::endl
std::cout <<std::boolalpha << "std::is_aggregate_v<Y> : " << std::is_aggregate_v<Y> <<
std::endl;
}
用C++17标准编译运行以上代码会输出:
std::is_aggregate_v<X> : true
std::is_aggregate_v<Y> : true
由此可见,虽然类X和Y都有用户声明的构造函数,但是它们依旧是聚合类型。不过这就引出了一个问题,让我们将目光放在结构体Y上,因为它的默认构造函数被显式地删除了,所以该类型应该无法实例化对象.
例如:
Y y1; // 编译失败,使用了删除函数
但是作为聚合类型,我们却可以通过聚合初始化的方式将其实例化:
Y y2{}; // 编译成功
编译成功的这个结果显然不是类型Y的设计者想看到的,而且这个问题很容易在真实的开发过程中被忽略,从而导致意想不到的结果。除了删除默认构造函数,将其列入私有访问中也会有同样的问题,比如:
struct Y
{
private:
Y() = default;
};
Y y1; // 编译失败,构造函数为私有访问
y y2{}; // 编译成功
请注意,这里Y() = default;中的= default不能省略,否
则Y会被识别为一个非聚合类型。
为了避免以上问题的出现,在C++17标准中可以使用explicit说明符或者将= default声明到结构体外,例如:
struct X
{
explicit X() = default;
};
struct Y
{
Y();
};
Y::Y() = default;
这样一来,结构体X和Y被转变为非聚合类型,也就无法使用聚合初始化了。不过即使这样,还是没有解决相同类型不同实例化方式表现不一致的尴尬问题
//C++17
#include <iostream>
struct X //NO aggregate
{
int val;
X() {}; //user-provided/defined constructor
};
struct Y // aggregate from C++11 until C++17
{
int val;
Y() = default; //user-declared, but not user-provided constructor
};
struct Z // aggregate from C++11 until C++17
{
int val;
Z() = delete; //user-declared, but not user-provided constructor
};
struct U // aggregate from C++11 until C++17
{
int val;
private:
U() = default; //user-declared, but not user-provided constructor
};
int main()
{
std::cout << std::boolalpha;
std::cout << "std::is_aggregate_v<X> : " << std::is_aggregate_v<X> << std::endl; //false
std::cout << "std::is_aggregate_v<Y> : " << std::is_aggregate_v<Y> << std::endl; //true
std::cout << "std::is_aggregate_v<Z> : " << std::is_aggregate_v<Z> << std::endl; //true
std::cout << "std::is_aggregate_v<U> : " << std::is_aggregate_v<U> << std::endl; //true
X x{};
Y y1;
Y y2{10}; // OK from C++11 until C++17
//Z z1; //error: use of deleted function 'Z::Z()
Z z2{}; // OK from C++11 until C++17
//U u1;//error: 'constexpr U::U()' is private within this contex
U u2{}; // OK from C++11 until C++17
}
struct Z 用户声明的构造函数是delete的,struct U的private构造函数是用户声明的不是用户提供的。
这种特殊的行为是在一个非常特殊的情况下引入的,但完全违反直觉。
然而,就连几名委员会成员也不知道这种行为,他们感到非常惊讶,甚至发现这种支持根本不需要。
C++20对聚合类的扩展
用户声明的构造函数
C++20 修正了这个问题,重新要求聚合没有用户声明的构造函数(就像C++11 之前的情况一样):
struct Y { // NO aggregate since C++20
int val{0};
Y() = default;//user-declared, but not user-provided constructor
};
struct Z { // NO aggregate since C++20
int val{0};
Z(int) = delete;//user-declared, but not user-provided constructor
};
struct U { // NO aggregate since C++20
int val{0};
private:
U() = default ;//user-declared, but not user-provided constructor
};
Y y2{};// OK
Z z2{}; // ERROR since C++20
U u2{}; // ERROR subce C++20
C++20中的这个修改会对之前的代码有影响:
下面代码中的 struct Y, Z, U都会受到影响。
#include <iostream>
struct X { //NO aggregate
int val;
X() {}; //user-provided/defined constructor
};
struct Y { //NO aggregate since C++20
int val;
Y() = default;//user-declared, but not user-provided constructor
};
struct Z { // NO aggregate since C++20
int val;
Z() = delete;//user-declared, but not user-provided constructor
};
struct U { // NO aggregate since C++20
int val;
private:
U() = default; //;//user-declared, but not user-provided constructor
};
int main(){
std::cout << std::boolalpha;
std::cout << "std::is_aggregate_v<X> : " << std::is_aggregate_v<X> << std::endl; //false
std::cout << "std::is_aggregate_v<Y> : " << std::is_aggregate_v<Y> << std::endl; //false
std::cout << "std::is_aggregate_v<Z> : " << std::is_aggregate_v<Z> << std::endl; //false
std::cout << "std::is_aggregate_v<U> : " << std::is_aggregate_v<U> << std::endl; //false
//X x{10};//error: no matching function for call to ‘X::X()’
Y y1;
//Y y2{10};//C++20 error: no matching function for call to ‘Y::Y()’
//Z z1; //error: use of deleted function 'Z::Z()
//Z z2{}; //C++20 error: use of deleted function ‘Z::Z()’
//U u1;//error: 'constexpr U::U()' is private within this contex
//U u2{}; //C++20 error: ‘constexpr U::U()’ is private within this context
}
总而言之,自C++20 起,聚合的定义如下:
• 一个数组
• 或者一个类型(类、结构或联合):
– 没有用户声明的构造函数
– using 声明没有继承构造函数
– 没有private 或proctected 的非静态数据成员
– 没有虚函数
– 没有virtual、private 或proctected 的基类
为了初始化聚合,需要应用以下附加限制:
• 没有private 或proctected 的基类成员
• 没有private 或proctected 的构造函数
聚合类的类模板参数推导
Class Template Argument Deduction (CTAD) for Aggregates
template<typename T>
struct Aggr {
T value;
};
Aggr<int> a1{42}; // OK
Aggr a2{42}; // ERROR before C++20, OK since C++20 even without deduction guide
For C++17, you had to provide a deduction guide:
template<typename T>
Aggr(T) -> Aggr<T>;
Aggr<int> a1{42}; // OK
Aggr a2{42}; // OK since C++17
Note that this feature also works when using parentheses to initialize aggregates:
Aggr a3(42); // OK since C++20 even without deduction guide
#include <iostream>
#include <string>
template<typename T>
struct Aggr {
T value;
};
template<typename T>
Aggr(T) -> Aggr<T>;
void test_cpp20()
{
Aggr<int> a1{42};
Aggr a2{42};// C++17 need deduction guide, C++20 ok.
//Aggr a3(42); OK since C++20
}
int main(void)
{
test_cpp20();
}
//gcc OK, clang 19.0 ok, cpp insight ok
#include <iostream>
#include <string>
template<typename... Ts>
struct overload : Ts...
{
using Ts::operator()...;
};
// C++17 deduction guide, not needed in C++20
//template<typename... Ts>
//overload(Ts...)->overload<Ts...>;
auto twice = overload{
[](std::string& s) { s += s; },
[](int& v) { v *= 2; }
};
int main()
{
int i = 1;
std::string s{ "hi"};
twice(i);
twice(s);
std::cout << "i=" << i << ", s=" << s << std::endl;
return 0;
}