C++ 23 实用工具(一)
工具函数是非常有价值的工具。它们不仅可以用于特定的领域,还可以应用于任意值和函数,甚至可以创建新的函数并将它们绑定到变量上。
常用函数
你可以使用各种变体的 min
、max
和 minmax
函数来对值和初始化列表进行操作。这些函数需要头文件 <algorithm>
。相反,std::move
、std::forward
、std::to_underlying
和 std::swap
函数则定义在头文件 <utility>
中,你可以将它们应用于任意值中。
以上是本章节中提供的一些实用工具函数和库,可以在各个领域和场景中灵活使用。希望这些函数和库能够帮助你更加高效地开发和管理项目。
std::min、std::max和std::minmax
在 C++ 的 <algorithm>
头文件中,有三个非常有用的函数:std::min
、std::max
和 std::minmax
。它们可以作用于值和初始化列表,并将所请求的值作为结果返回。对于 std::minmax
函数,你会得到一个 std::pair
,其中第一个元素是最小值,第二个元素是最大值。默认情况下使用小于运算符(<
),但你可以应用自己的比较运算符。这个函数需要两个参数并返回一个布尔值。这种返回 true
或 false
的函数称为谓词。
int main()
{std::cout << std::min(2011, 2022) << std::endl; // 2011std::cout << std::min({3, 1, 2011, 2022, -5}) << std::endl; // -5std::cout << std::min(-10, -5, [](int a, int b) { return std::abs(a) < std::abs(b); }) << std::endl; // -5auto pairInt = std::minmax(2011, 2022);auto pairSeq = std::minmax({3, 1, 2011, 2022, -5});auto pairAbs = std::minmax({3, 1, 2011, 2022, -5}, [](int a, int b) { return std::abs(a) < std::abs(b); });std::cout << pairInt.first << "," << pairInt.second << std::endl; // 2011,2022std::cout << pairSeq.first << "," << pairSeq.second << std::endl; // -5,2022std::cout << pairAbs.first << "," << pairAbs.second << std::endl; // 1,2022return 0;
}
函数 | 描述 |
---|---|
min(a, b) | 返回 a 和 b 中的较小值。 |
min(a, b, comp) | 根据谓词 comp 返回 a 和 b 中的较小值。 |
min(initializer list) | 返回初始化列表中的最小值。 |
min(initializer list, comp) | 根据谓词 comp 返回初始化列表中的最小值。 |
max(a, b) | 返回 a 和 b 中的较大值。 |
max(a, b, comp) | 根据谓词 comp 返回 a 和 b 中的较大值。 |
max(initializer list) | 返回初始化列表中的最大值。 |
max(initializer list, comp) | 根据谓词 comp 返回初始化列表中的最大值。 |
minmax(a, b) | 返回 a 和 b 中的较小值和较大值。 |
minmax(a, b, comp) | 根据谓词 comp 返回 a 和 b 中的较小值和较大值。 |
minmax(initializer list) | 返回初始化列表中的最小值和最大值。 |
minmax(initializer list, comp) | 根据谓词 comp 返回初始化列表中的最小值和最大值。 |
std::midpoint 和 std::lerp
std::midpoint(a, b)
函数计算 a
和 b
的中点。a
和 b
可以是整数、浮点数或指针。如果 a
和 b
是指针,则必须指向同一数组对象。std::midpoint
函数需要头文件 <numeric>
。
std::lerp(a, b, t)
函数计算两个数的线性插值。它需要头文件 <cmath>
。返回值为 a + t(b - a)
。
线性插值是一种常见的数值分析技术,用于在两个已知数据点之间估算未知点的值。它可以用于许多应用程序,例如图像处理、计算机图形学和动画。其中一个常见的用途是在两个颜色之间进行插值,以创建渐变效果。
#include <iostream>
#include <numeric>
#include <cmath>int main()
{// midpointstd::cout << std::midpoint(10, 20) << std::endl; // 15std::cout << std::midpoint(10.5, 20.5) << std::endl; // 15.5int arr[] = {1, 2, 3, 4, 5};int* p1 = &arr[0];int* p2 = &arr[4];std::cout << *std::midpoint(p1, p2) << std::endl; // 3// lerpstd::cout << std::lerp(10, 20, 0.5) << std::endl; // 15std::cout << std::lerp(10.5, 20.5, 0.5) << std::endl; // 15.5return 0;
}
std::cmp_equal, std::cmp_not_equal, std::cmp_less, std::cmp_greater, std::cmp_less_equal 和 std::cmp_greater_equal
头文件 <utility>
中定义的函数 std::cmp_equal
、std::cmp_not_equal
、std::cmp_less
、std::cmp_greater
、std::cmp_less_equal
和 std::cmp_greater_equal
提供整数的安全比较。安全比较意味着负有符号整数与无符号整数进行比较时,负有符号整数的比较小于无符号整数,并且除有符号或无符号整数之外的其他值的比较会在编译时出错。
以下是一个代码片段,演示了有符号/无符号比较的问题。
-1 < 0u; // true
std::cmp_greater(-1, 0u); // false
将 -1 视为有符号整数时,它被提升为无符号类型,这导致了一个令人惊讶的结果。
std::move
头文件 <utility>
中定义的 std::move
函数授权编译器移动资源。在所谓的移动语义中,源对象的值被移动到新对象中。之后,源对象处于一个定义良好但不确定的状态。大多数情况下,这是源对象的默认状态。使用 std::move
,编译器将源参数转换为右值引用:static_cast<std::remove_reference<decltype(arg)>::type&&>(arg)
。如果编译器无法应用移动语义,则回退到复制语义:
#include <utility>
// ...
std::vector<int> myBigVec(10000000, 2011);
std::vector<int> myVec;
myVec = myBigVec; // 复制语义
myVec = std::move(myBigVec); // 移动语义
移动语义可以提高程序的性能,因为它避免了不必要的内存分配和复制操作。但是请注意,移动语义只适用于具有可移动的资源的对象,例如指针、文件句柄和大型数组。对于简单类型,移动语义通常不会提供任何性能优势。
std::forward
std::forward是定义在头文件中的一个函数,它可以让你编写函数模板,以完全相同的方式转发它们的参数。使用std::forward的典型用例是工厂函数或构造函数。工厂函数创建一个对象,因此应该传递它们的参数而不作任何修改。构造函数通常使用它们的参数来使用相同的参数初始化它们的基类。因此,std::forward是通用库作者的完美工具。
createT是一个函数模板,它必须将它们的参数作为通用引用(Args&&… args)接收。通用引用或转发引用是一个类型推断上下文中的右值引用。std::forward与可变模板一起使用,可以定义完全通用的函数模板。您的函数模板可以接受任意数量的参数并将它们不变地转发。以下是createT的示例用法:
#include <utility>
using std::initialiser_list;struct MyData{MyData(int, double, char){};
};template <typename T, typename... Args>
T createT(Args&&... args){return T(std::forward<Args>(args)... );
}int a = createT<int>();
int b = createT<int>(1);
std::string s = createT<std::string>("Only for testing.");
MyData myData2 = createT<MyData>(1, 3.19, 'a');
typedef std::vector<int> IntVec;
IntVec intVec = createT<IntVec>(initialiser_list<int>({1, 2, 3}));
以上是使用std::forward和createT示例的代码。这段代码定义了一个MyData结构体以及一个createT函数模板,用于创建各种类型的对象。createT函数模板使用std::forward将其参数转发到T类型的构造函数中,以创建T类型的对象。这使得createT函数模板可以用于创建任何类型的对象,从基本类型到自定义类型。
#include <utility>
#include <vector>
#include <string>
#include <initializer_list>using std::initializer_list;struct MyData
{MyData(int, double, char){};
};template <typename T, typename... Args>
T createT(Args&&... args)
{return T(std::forward<Args>(args)...);
}int main()
{int a = createT<int>();int b = createT<int>(1);std::string s = createT<std::string>("Only for testing.");MyData myData2 = createT<MyData>(1, 3.19, 'a');typedef std::vector<int> IntVec;IntVec intVec = createT<IntVec>(initializer_list<int>({1, 2, 3}));return 0;
}
使用std::forward实现完全通用的函数模板
在 C++ 中,使用模板可以实现通用的函数,但是当函数需要接受任意类型的参数时,需要使用可变模板参数(variadic templates)和通用引用(universal reference)。
通用引用是指在类型推导上下文中的右值引用,其语法为 Args&&… args
。它可以接受任意类型(左值或右值)的参数,并保持完整性。在函数模板中使用通用引用时,需要使用 std::forward
函数来实现参数的完美转发。
std::forward
是一个 C++11 的模板函数,它可以将一个参数以右值或左值的形式进行转发。通常用于在一个函数中将参数转发到另一个函数,以实现参数的完美转发。
使用 std::forward
和可变模板参数,我们可以定义完全通用的函数模板。函数模板可以接受任意数量和类型的参数,并将它们完美转发到另一个函数中。这样可以大大提高代码的复用性和灵活性。
std::to_underlying
在 C++23 中,函数 std::to_underlying
将枚举类型 enum
转换为其基础类型。这个函数是一个便利函数,其表达式为 static_cast<std::underlying_type<Enum>::type>(enum)
,使用了类型特征函数 std::underlying_type
。
enum class Color { RED, GREEN, BLUE };
Color c = Color::RED;
std::underlying_type_t<Color> cu = std::to_underlying(c);
上面的代码将枚举类型 Color
的值 Color::RED
转换为其基础类型 std::underlying_type_t<Color>
。
std::swap
使用头文件 <utility>
中定义的函数 std::swap
,您可以轻松交换两个对象。C++ 标准库中的通用实现内部使用函数 std::move
。
下面是带有移动语义的 std::swap
的示例实现代码:
#include <utility>template <typename T>
inline void swap(T& a, T& b) noexcept {T tmp(std::move(a));a = std::move(b);b = std::move(tmp);
}
上面的代码定义了一个模板函数 swap
,可以交换两个类型为 T
的对象。该函数内部使用了 std::move
来实现移动语义,从而提高了效率