C++之聚合类扩展(C++17/20)

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值