现代C++语言核心特性解析part8

第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都还没有支持该特性。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
现代 C 语言是指 C11/C17 标准之后的 C 语言版本。其核心特性包括了多线程、原子操作、泛型和 stdatomic 库等。下面具体解析一下现代 C 语言核心特性。 1. 多线程 多线程是现代 C 语言中最重要的特性之一。多线程使得程序可以同时执行多个任务,从而提高程序性能。现代 C 语言通过 POSIX 线程库和 Windows 线程库实现多线程编程。POSIX 线程库是 POSIX 标准中定义的线程库,可以跨平台使用。Windows 线程库是 Windows 操作系统中的线程库,可以在 Windows 系统上使用。 2. 原子操作 原子操作是现代 C 语言中的另一个重要特性。原子操作可以保证多线程环境下的数据、变量等在并发访问时不会出错。现代 C 语言提供了一组原子操作的 API。这些 API 包括了原子加、原子减、原子赋值、原子与、原子或等。这些原子操作可以在不同的平台上使用。 3. 泛型 现代 C 语言引入了泛型的概念。泛型允许程序员在不同的数据类型上编写通用的代码。这使得现代 C 语言可以实现更加通用的数据结构和算法。现代 C 语言中的泛型使用了类型参数化的技术,即把某些代码中的类型抽象出来作为参数。 4. stdatomic 库 stdatomic 库是现代 C 语言中的一个库,它提供了原子类型和原子操作等特性。stdatomic 库使用了 C11 标准中的 _Atomic 关键字来定义原子类型。stdatomic 库中包含了原子加、原子减、原子赋值等操作函数,这些操作可以在多线程环境下执行,保证数据的正确性。 总之,现代 C 语言核心特性包括了多线程、原子操作、泛型和 stdatomic 库等。这些特性使得现代 C 语言可以更好地支持并发编程、泛型编程等。开发者可以充分利用这些特性来提高程序性能和可维护性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值