第14章 强枚举类型(C++11 C++17 C++20)
14.1 枚举类型的弊端
enum类型破坏了C++的类型安全。
首先,虽然枚举类型存在一定的安全检查功能,一个枚举类型不允许分配到另外一种枚举类型,而且整型也无法隐式转换成枚举类型。但是枚举类型却可以隐式转换为整型:
enum School {
principal,
teacher,
student
};
enum Company {
chairman,
manager,
employee
};
int main()
{
School x = student;
Company y = manager;
bool b = student >= manager; // 不同类型之间的比较操作
b = x < employee;
int y = student; // 隐式转换为int
}
然后是枚举类型的作用域问题,枚举类型会把其内部的枚举标识符导出到枚举被定义的作用域。
enum HighSchool {
student,
teacher,
principal
};
enum University {
student,
professor,
principal
};
HighSchool和University都有student和principal,而枚举类型又会将其枚举标识符导出到定义它们的作用域,这样就会发生重复定义,无法通过编译。
解决此类问题的一个办法是使用命名空间,例如:
enum HighSchool {
student,
teacher,
principal
};
namespace AcademicInstitution
{
enum University {
student,
professor,
principal
};
}
对于上面两个问题,有一个比较好但并不完美的解决方案,代码如下:
#include <iostream>
class AuthorityType {
enum InternalType
{
ITBan,
ITGuest,
ITMember,
ITAdmin,
ITSystem,
};
InternalType self_;
public:
AuthorityType(InternalType self) : self_(self) {}
bool operator < (const AuthorityType &other) const
{
return self_ < other.self_;
}
bool operator > (const AuthorityType &other) const
{
return self_ > other.self_;
}
bool operator <= (const AuthorityType &other) const
{
return self_ <= other.self_;
}
bool operator >= (const AuthorityType &other) const
{
return self_ >= other.self_;
}
bool operator == (const AuthorityType &other) const
{
return self_ == other.self_;
}
bool operator != (const AuthorityType &other) const
{
return self_ != other.self_;
}
const static AuthorityType System, Admin, Member, Guest, Ban;
};
#define DEFINE_AuthorityType(x) const AuthorityType \
AuthorityType::x(AuthorityType::IT ## x)
DEFINE_AuthorityType(System);
DEFINE_AuthorityType(Admin);
DEFINE_AuthorityType(Member);
DEFINE_AuthorityType(Guest);
DEFINE_AuthorityType(Ban);
int main()
{
bool b = AuthorityType::System > AuthorityType::Admin;
std::cout << std::boolalpha << b << std::endl;
}
将枚举类型变量封装成类私有数据成员,保证无法被外界访问。访问枚举类型的数据成员必须通过对应的常量静态对象。另外,根据C++标准的约束,访问静态对象必须指明对象所属类型。也就是说,如果我们想访问ITSystem这个枚举标识符,就必须访问常量静态对象System,而访问System对象,就必须说明其所属类型,这使我们需要将代码写成AuthorityType:: System才能编译通过。
还有一个严重的问题是,无法指定枚举类型的底层类型。
enum E {
e1 = 1,
e2 = 2,
e3 = 0xfffffff0
};
int main()
{
bool b = e1 < e3;
std::cout << std::boolalpha << b << std::endl;
}
在GCC中,结果返回true,我们可以认为E的底层类型为unsigned int。如果输出e3,会发现其值为4294967280。但是在MSVC中结果输出为false,很明显在编译器内部将E定义为了int类型,输出e3的结果为−16。这种编译器上的区别会使在编写跨平台程序时出现重大问题。
值得一提的是,枚举类型缺乏类型检查的问题倒是成就了一种特殊用法。如果读者了解模板元编程,那么肯定见过一种被称为enumhack的枚举类型的用法。简单来说就是利用枚举值在编译期就能确定下来的特性,让编译器帮助我们完成一些计算:
#include <iostream>
template<int a, int b>
struct add {
enum {
result = a + b
};
};
int main()
{
std::cout << add<5, 8>::result << std::endl;
}
用GCC查看其GIMPLE的中间代码:
main ()
{
int D.39267;
_1 = std::basic_ostream<char>::operator<< (&cout, 13);
std::basic_ostream<char>::operator<< (_1, endl);
D.39267 = 0;
return D.39267;
}
14.2 使用强枚举类型
在C++11标准中对其做出了重大升级,增加了强枚举类型。强枚举类型具备以下3个新特性。
1.枚举标识符属于强枚举类型的作用域。
2.枚举标识符不会隐式转换为整型。
3.能指定强枚举类型的底层类型,底层类型默认为int类型。
#include <iostream>
enum class HighSchool {
student,
teacher,
principal
};
enum class University {
student,
professor,
principal
};
int main()
{
HighSchool x = HighSchool::student;
University y = University::student;
bool b = x < HighSchool::teacher;
std::cout << std::boolalpha << b << std::endl;
}
HighSchool x = student; // 编译失败,找不到student的定义
bool b = University::student < HighSchool::teacher;// 编译失败,比较的类型不同
int y = University::student; // 编译失败,无法隐式转换为int类型
对于强枚举类型的第三个特性,我们可以在定义类型的时候使用:符号来指明其底层类型。利用它可以消除不同编译器带来的歧义:
#include <iostream>
enum class E : unsigned int {
e1 = 1,
e2 = 2,
e3 = 0xfffffff0
};
int main()
{
bool b = E::e1 < E::e3;
std::cout << std::boolalpha << b << std::endl;
}
14.3 列表初始化有底层类型枚举对象
从C++17标准开始,对有底层类型的枚举类型对象可以直接使用列表初始化。
enum class Color {
Red,
Green,
Blue
};
int main()
{
Color c{ 5 }; // 编译成功
Color c1 = 5; // 编译失败
Color c2 = { 5 }; // 编译失败
Color c3(5); // 编译失败
}
同样的道理,下面的代码能编译通过:
enum class Color1 : char {};
enum Color2 : short {};
int main()
{
Color1 c{ 7 };
Color2 c1{ 11 };
Color2 c2 = Color2{ 5 };
}
对于Color2 c2 = Color2{ 5 }来说,代码先通过列表初始化了一个临时对象,然后再赋值到c2,而Color c2 = { 5 }则没有这个过程。
没有指定底层类型的枚举类型是无法使用列表初始化的,比如:
enum Color3 {};
int main()
{
Color3 c{ 7 };
}
同所有的列表初始化一样,它禁止缩窄转换,所以下面的代码也是不允许的:
enum class Color1 : char {};
int main()
{
Color1 c{ 7.11 };
}
C++17为有底层类型的枚举类型放宽了初始化的限制,让其支持列表初始化:
#include <iostream>
enum class Index : int {};
int main()
{
Index a{ 5 };
Index b{ 10 };
// a = 12;
// int c = b;
std::cout << "a < b is "
<< std::boolalpha
<< (a < b) << std::endl;
}
定义了Index的底层类型为int,所以可以使用列表初始化a和b,由于a和b的枚举类型相同,因此所有a < b的用法也是合法的。但是a = 12和int c = b无法成功编译,因为强枚举类型是无法与整型隐式相互转换的。
14.4 使用using打开强枚举类型
C++20标准扩展了using功能,它可以打开强枚举类型的命名空间。
enum class Color {
Red,
Green,
Blue
};
const char* ColorToString(Color c)
{
switch (c)
{
case Color::Red: return "Red";
case Color::Green: return "Green";
case Color::Blue: return "Blue";
default:
return "none";
}
}
通过using我们可以简化这部分代码:
enum class Color {
Red,
Green,
Blue
};
const char* ColorToString(Color c)
{
switch (c)
{
using enum Color;
case Red: return "Red";
case Green: return "Green";
case Blue: return "Blue";
default:
return "none";
}
}
除了引入整个枚举标识符之外,using还可以指定引入的标识符,例如:
const char* ColorToString(Color c)
{
switch (c)
{
using Color::Red;
case Red: return "Red";
case Color::Green: return "Green";
case Color::Blue: return "Blue";
default:
return "none";
}
}
第15章 扩展的聚合类型(C++17 C++20)
15.1 聚合类型的新定义
C++17标准对聚合类型的定义做出了大幅修改,即从基类公开且非虚继承的类也可能是一个聚合。同时聚合类型还需要满足常规条件。
1.没有用户提供的构造函数。
2.没有私有和受保护的非静态数据成员。
3.没有虚函数。
在新的扩展中,如果类存在继承关系,则额外满足以下条件。
4.必须是公开的基类,不能是私有或者受保护的基类。
5.必须是非虚继承。
在标准库<type_traits>中提供了一个聚合类型的甄别办法is_aggregate,它可以帮助我们判断目标类型是否为聚合类型:
#include <iostream>
#include <string>
class MyString : public std::string {};
int main()
{
std::cout << "std::is_aggregate_v<std::string> = "
<< std::is_aggregate_v<std::string> << std::endl;
std::cout << "std::is_aggregate_v<MyString> = "
<< std::is_aggregate_v<MyString> << std::endl;
}
输出结果:
std::is_aggregate_v<std::string> = 0
std::is_aggregate_v<MyString> = 1
在上面的代码中,先通过std::is_aggregate_v判断std::string是否为聚合类型,根据我们对std::string的了解,它存在用户提供的构造函数,所以一定是非聚合类型。然后判断类MyString是否为聚合类型,虽然该类继承了std::string,但因为它是公开继承且是非虚继承,另外,在类中不存在用户提供的构造函数、虚函数以及私有或者受保护的数据成员,所以MyString应该是聚合类型。
15.2 聚合类型的初始化
过去要想初始化派生类的基类,需要在派生类中提供构造函数
#include <iostream>
#include <string>
class MyStringWithIndex : public std::string {
public:
MyStringWithIndex(const std::string& str, int idx) :
std::string(str), index_(idx) {}
int index_ = 0;
};
std::ostream& operator << (std::ostream &o, const
MyStringWithIndex& s)
{
o << s.index_ << ":" << s.c_str();
return o;
}
int main()
{
MyStringWithIndex s("hello world", 11);
std::cout << s << std::endl;
}
输出结果:
11:hello world
由于聚合类型的扩展,这个过程得到了简化。需要做的修改只有两点,第一是删除派生类中用户提供的
构造函数,第二是直接初始化:
#include <iostream>
#include <string>
class MyStringWithIndex : public std::string {
public:
int index_ = 0;
};
std::ostream& operator << (std::ostream &o, const
MyStringWithIndex& s)
{
o << s.index_ << ":" << s.c_str();
return o;
}
int main()
{
MyStringWithIndex s{ {"hello world"}, 11 };
std::cout << s << std::endl;
}
MyStringWithIndex s{ {“hello world”}, 11}是典型的初始化基类聚合类型的方法。其中{“hello world”}用于基类的初始化,11用于index_的初始化。
如果派生类存在多个基类,那么其初始化的顺序与继承的顺序相同:
#include <iostream>
#include <string>
class Count {
public:
int Get() { return count_++; }
int count_ = 0;
};
class MyStringWithIndex : public std::string, public Count {
public:
int index_ = 0;
};
std::ostream& operator << (std::ostream &o, MyStringWithIndex& s)
{
o << s.index_ << ":" << s.Get() << ":" << s.c_str();
return o;
}
int main()
{
MyStringWithIndex s{ "hello world", 7, 11 };
std::cout << s << std::endl;
std::cout << s << std::endl;
}
输出结果:
11:7:hello world
11:8:hello world
{ “hello world”, 7, 11}中字符串"hello world"对应基类std::string,7对应基类Count,11对应数据成员index_。
15.3 扩展聚合类型的兼容问题
虽然扩展的聚合类型给我们提供了一些方便,但同时也带来了一个兼容老代码的问题
#include <iostream>
#include <string>
class BaseData {
int data_;
public:
int Get() { return data_; }
protected:
BaseData() : data_(11) {}
};
class DerivedData : public BaseData {
public:
};
int main()
{
DerivedData d{};
std::cout << d.Get() << std::endl;
}
以上代码使用C++11或者C++14标准可以编译成功,而使用C++17标准编译则会出现错误,主要原因就是聚合类型的定义发生了变化。从C++17开始情况发生了变化,类DerivedData变成了一个聚合类型,以至于DerivedData d{}也跟着变成聚合类型的初始化,因为基类BaseData中的构造函数是受保护的关系,它不允许在聚合类型初始化中被调用,所以编译器无奈之下给出了一个编译错误。在更新开发环境到C++17标准的时候遇到了这样的问题,只需要为派生类提供一个默认构造函数即可。
15.4 禁止聚合类型使用用户声明的构造函数
用户提供的构造函数和用户声明的构造函数是有区别的,比如:
#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::is_aggregate_v<Y> : " << std::is_aggregate_v<Y> <<
std::endl;
}
输出结果:
std::is_aggregate_v<X> : true
std::is_aggregate_v<Y> : true
虽然类X和Y都有用户声明的构造函数,但是它们依旧是聚合类型。
不过这就引出了一个问题,让我们将目光放在结构体Y上,因为它的默认构造函数被显式地删除了,所以该类型应该无法实例化对象
Y y1; // 编译失败,使用了删除函数
但是作为聚合类型,我们却可以通过聚合初始化的方式将其实例化:
Y y2{};
除了删除默认构造函数,将其列入私有访问中也会有同样的问题,比如:
struct Y {
private:
Y() = default;
};
Y y1; // 编译失败,构造函数为私有访问
y y2{}; // 编译成功
为了避免以上问题的出现,在C++17标准中可以使用explicit说明符或者将= default声明到结构体外
struct X {
explicit X() = default;
};
struct Y {
Y();
};
Y::Y() = default;
结构体X和Y被转变为非聚合类型,也就无法使用聚合初始化了。
在C++20标准中禁止聚合类型使用用户声明的构造函数,这种处理方式让所有的情况保持一致,是最为简单明确的方法。
#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::is_aggregate_v<Y> : " << std::is_aggregate_v<Y> <<
std::endl;
}
输出结果:
std::is_aggregate_v<X> : false
std::is_aggregate_v<Y> : false
这个规则的修改会改变一些旧代码的意义,比如我们经常用到的禁止复制构造的方法:
struct X {
std::string s;
std::vector<int> v;
X() = default;
X(const X&) = delete;
X(X&&) = default;
};
上面这段代码中结构体X在C++17标准中是聚合类型,所以可以使用聚合类型初始化对象。但是升级编译环境到C++20标准会使X转变为非聚合对象,从而造成无法通过编译的问题。
一个可行的解决方案是,不要直接使用= delete;来删除复制构造函数,而是通过加入或者继承一个不可复制构造的类型来实现类型的不可复制,例如:
struct X {
std::string s;
std::vector<int> v;
[[no_unique_address]] NonCopyable nc;
};
// 或者
struct X : NonCopyable {
std::string s;
std::vector<int> v;
};
15.5 使用带小括号的列表初始化聚合类型对象
使用带小括号的列表初始化聚合类型对象
struct X {
int i;
float f;
};
X x{ 11, 7.0f };
如果将上面初始化代码中的大括号修改为小括号,C++17标准的编译器会给出无法匹配到对应构造函数X::X(int, float)的错误,这说明小括号会尝试调用其构造函数。这一点在C++20标准中做出了修改,它规定对于聚合类型对象的初始化可以用小括号列表来完成,其最终结果与大括号列表相同。
X x( 11, 7.0f );
带大括号的列表初始化是不支持缩窄转换的,但是带小括号的列表初始化却是支持缩窄转换的
struct X {
int i;
short f;
};
X x1{ 11, 7.0 }; // 编译失败,7.0从double转换到short是缩窄转换
X x2( 11, 7.0 ); // 编译成功
到目前为止该特性只在GCC中得到支持,而CLang和MSVC都还没有支持该特性。