【C++11】一些易用性的改进
Ⅰ. for_each
这个之前我们讲过了,其实本质上就是迭代器的封装!这里就不细讲了,只要注意一点:使用基于范围的 for
循环, 其 for
循环迭代的范围必须是可确定的
void func(int arr[]) // 形参中的数组不是数组,而是指针变量,存放数组的地址,无法确定元素个数
{
// ❌使用基于范围的for循环,其for循环迭代的范围必须是可确定的
for (int& tmp : arr)
std::cout << tmp << " ";
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
func(arr);//传过去的是arr数组的地址
return 0;
}
Ⅱ. 静态断言 static_assert
C++
中的静态断言 static_assert
是一个编译时断言,用于在编译期间检查某个条件是否为真。如果条件为假,则会导致编译错误,否则不会产生任何代码。
静态断言的语法如下:
代码语言:javascript代码运行次数:0运行复制static_assert(condition, optional message);
其中,condition
是一个表达式,如果表达式的结果为 false
,编译器会报错;optional message
是可选的,用于提供编译器报错时的错误信息。
注意: 只能是常量表达式,不能是变量 。
下面是一个使用静态断言的示例:
代码语言:javascript代码运行次数:0运行复制#include <type_traits>
template <class T>
void do_something(T value)
{
// 如果T不是整数类型,编译器会在这里报错
static_assert(std::is_integral<T>::value, "T must be an integral type");
// 此处省略具体的代码实现
...
}
int main()
{
do_something(42); // 正常调用
do_something("hello"); // 编译错误:T must be an integral type
return 0;
}
在上面的示例中,do_something
函数使用了静态断言来确保传入的类型 T
是整数类型。如果 T
不是整数类型,编译器会在函数体内部报错,从而避免运行时错误。
静态断言的好处:
- 更早的报告错误, 我们知道构建是早于运行的, 更早的错误报告意味着开发成本的降低。
- 减少运行时调用堆栈开销, 静态断言是编译期检测的, 减少了运行时开销 。
Ⅲ. 异常处理 noexcept
一、异常处理回顾
C++
中的异常是一种用于处理程序运行过程中出现错误的机制。当程序执行过程中出现错误时,可以通过抛出异常来告诉程序的调用者发生了错误,然后通过捕获异常来处理错误。
下面是一个使用异常处理的示例:
代码语言:javascript代码运行次数:0运行复制#include <iostream>
#include <stdexcept>
void divide(int x, int y)
{
if (y == 0)
throw std::invalid_argument("divide by zero"); // 抛出异常
std::cout << "result = " << x / y << std::endl;
}
int main()
{
try {
divide(10, 0); // 调用可能抛出异常的函数
} catch (const std::exception& e) {
std::cerr << "Exception caught: " << e.what() << std::endl; // 捕获异常并处理
}
return 0;
}
// 运行结果
Exception caught: divide by zero
在上面的示例中,divide()
函数用于计算两个数的商,如果除数为 0
,则抛出一个 std::invalid_argument
类型的异常,然后被 catch
语句捕获并处理,输出错误信息。如果除数不为 0
,divide()
函数会正常执行,输出结果。
需要注意的是,抛出异常会使程序跳出当前函数,并沿着函数调用链向上查找 try-catch
语句,直到找到能够处理该异常的 catch
语句。
如果没有找到能够处理该异常的 catch
语句,则程序会异常终止。因此,在使用异常处理时,应该确保抛出异常的地方能够被 catch
语句捕获,并且 不要滥用异常处理,因为在一些性能要求较高的场景下,异常处理可能会影响程序的性能,应该谨慎使用。
首先强调一点:
- 当
noexcept
作为 标识符 时,它的作用是在函数后面 声明一个函数是否会抛出异常。 - 当
noexcept
作为 函数 时,它的作用是 检查一个函数是否会抛出异常。
二、引入 noexcept
C++11
中引入了 noexcept
标识符,用于指示函数不会抛出任何异常,如果抛出异常, 那么程序就会异常终止。也就是说 noexcept
函数执行时出了异常,程序会马上异常终止。
在 C++
中,异常处理是一项昂贵的操作,因为需要构建完整的堆栈跟踪信息。所以 noexcept
主要是解决的问题是减少运行时开销,同时也可以提高代码的可靠性。运行时开销指的是编译器需要为代码生成一些额外的代码用来包裹原始代码,当出现异常时可以抛出一些相关的堆栈 stack unwinding
错误信息,这里面包含错误位置、错误原因、调用顺序和层级路径等信息。使用 noexcept
声明一个函数不会抛出异常,编译器就不会去生成这些额外的代码,直接的减小的生成文件的大小,间接的优化了程序运行效率。
注意:如果函数中存在任何可能抛出异常的操作,则不应该使用 noexcept
!
作为标识符有几种写法:
noexcept
:默认表示为下面的noexcept(true)
。noexcept(true)
:表示函数不可能抛出异常,如果抛异常了程序直接异常终止。noexcept(false)
:表示函数可能会抛出异常。throw()
:表示函数可能会抛出异常,不建议使用该写法,应该使用noexcept(false)
,因为C++20
放弃这种写法。
noexcept(常量表达式)
:用于判断表达式是否不会抛出异常。
void example() noexcept // noexcept相当于noexcept(true)
{
cout << "hello called" << endl;
}
在这个例子中,我们声明了一个函数 example()
并使用了 noexcept
。这意味着,函数 example()
不会抛出任何异常。如果在函数运行时发生了异常,程序会被终止。
另外这是 noexcept(常量表达式)
的使用方式,用于判断表达式是否不会抛出异常:
#include <iostream>
using std::cout;
using std::endl;
using std::boolalpha;
void foo() noexcept(true)
{
throw 4;
}
void bar() noexcept(false)
{
throw 4;
}
int main()
{
// 使用noexcept(常量表达式)判断foo()和bar()是否不会抛异常
cout << boolalpha << noexcept(foo()) << endl;
cout << boolalpha << noexcept(bar()) << endl;
return 0;
}
// 执行结果:
true
false
三、noexcept 函数
noexcept
函数在常规函数中配合 noexcept(expression)
标识符共同完成对其他函数是否声明了 noexcept
的检查,如下所示:
void swap(Type& x, Type& y) noexcept(noexcept(x.swap(y)))
{
x.swap(y);
}
它表示,如果操作 x.swap(y)
不发生异常,那么函数 swap(Type& x, Type& y)
一定不发生异常。
一个更好的示例是 std::pair
中的移动赋值函数,它表明,如果类型 T1
和 T2
的移动赋值过程中不发生异常,那么该移动构造函数就不会发生异常,如下所示:
pair& operator=(pair&& __p) noexcept(__and_<is_nothrow_move_assignable<_T1>,
is_nothrow_move_assignable<_T2>>::value)
{
first = std::forward<first_type>(__p.first);
second = std::forward<second_type>(__p.second);
return *this;
}
四、什么时候该使用 noexcept❓
使用 noexcept
表明函数或操作不会发生异常,会给编译器更大的优化空间。然而,并不是加上 noexcept
就能提高效率,步子迈大了也容易扯着蛋!以下情形鼓励使用 noexcept
:
- 移动构造函数
- 移动赋值函数
- 析构函数。这里提一句,在新版本的编译器中,析构函数是默认加上关键字
noexcept
的。 - 叶子函数。叶子函数是指在函数内部不分配栈空间,也不调用其它函数,也不存储非易失性寄存器,也不处理异常。
比如下面代码可以检测编译器是否给析构函数加上关键字 noexcept
:
struct X
{
~X() { };
};
int main()
{
X x;
static_assert(noexcept(x.~X()), "Ouch!");
}
五、一些大佬的建议
- 当对是否需要用
noexcept
有疑问时,选择不用。 noexcept
只是一个优化相关的东西,不用的话并不影响代码的正确性。- 通常情况下,在广泛使用
STL
容器、智能指针的现代C++
风格下,编译器能够推导自动生成的析构函数,移动构造和赋值运算符的noexcept
属性。 noexcept
判断比较复杂,业务代码程序员更关注业务逻辑本身,而且需求变化大,代码可能很复杂,人工判断很容易出错。- 影响接口的灵活性,比如基类某个虚函数设置为
noexcept
,派生类覆盖虚函数时也必须遵守,这个有些情况下难以保证。 - 用错了危害很大,会强行终止程序,本来能处理的都没有处理机会了。
- 就像异常规格的存在版本问题一样,如果一个函数从
noexcept
变为noexcept(false)
,调用处可能也需要跟着改动。 C++17
后,noexcept
还影响了函数的签名,进一步影响了代码的复杂性和兼容性。
Ⅳ. 强类型枚举(枚举类)
C++11
中引入了强类型枚举(strongly-typed enum
),也称为枚举类(enum class
),用于替代传统的 C++
枚举类型。
传统枚举类型的值是整数类型,并且在枚举作用域中具有全局可见性,所以 传统枚举类型容易造成命名冲突和类型不安全 等问题。而强类型枚举使用类的语法定义枚举类型,可以避免这些问题,并提供更好的类型安全性和可读性。