C++标准里的最小值和最大值函数
一、简介
最小值和最大值是非常简单的函数,没有太多可说的,真的是这样吗?
最小值和最大值是非常基本的概念,但也可能存在一些细节上的问题和需要注意的地方。本文将深入探讨C++标准库里的std::min
、std::max
等相关函数的用法和注意事项。
二、基本算法
2.1、min 和 max
从功能上讲,std::min
和std::max
确实在做很简单的事情:它们接受两个值,std::min
返回两个值中较小的那个,std::max
返回两个值中较大的那个。
函数原型:
template<typename T>
const T& min(const T& a, const T& b);
template<typename T>
const T& max(const T& a, const T& b);
注意返回类型是一个引用(指向const
)。即可以实例化指向较小或较大值的引用,从而避免进行复制。因为它是const
,所以不能通过这个引用修改原始值。
int a = 2;
int b = 3;
const int& minValue = std::min(a, b);
这个示例中取最小值时不会复制任何对象。对于整型类型来说,这有点过分,但对于一般情况来说,知道这一点很有用。
不过要小心引用的生命周期!如果将作为最小值的值销毁了,那么引用将指向一个不再存在的对象。即使 minValue
是一个指向常量的引用,情况也是如此。实际上,由常量引用保存的临时对象是来自 std::min
的对象,而不是传递进来的那个对象。为了说明这一点,可以看一下下面代码:
#include<iostream>
#include <algorithm>
int get2()
{
return 2;
}
int get3()
{
return 3;
}
int main(){
const int& minValue = std::min(get2(), get3());
std::cout << minValue << '\n';
return 0;
}
我们可能期望这段代码会显示 2,但实际上它会进入未定义行为。确实,std::min
产生的临时变量被const
引用保留了下来,但get2
产生的临时变量在std::min
执行后就被销毁了。
2.2、min_element 和 max_element
std::min
和std::max
有对应的*_element
来操作值的范围:std::min_element
和std::max_element
。
std::min
和std::max
返回的是较大和较小的值,而std::min_element
和std::max_element
以迭代器的形式返回较大和较小元素的位置:
template<typename ForwardIt>
ForwardIt min_element(ForwardIt first, ForwardIt last);
这种需求在代码中偶尔会出现。当出现这种情况时,不需要使用for
循环重新实现它:它就存在于STL中,等待被使用。所有这些算法都有接受自定义比较函数(或函数对象)的重载版本,以便将值与小于运算符以外的其他内容进行比较。
三、现代C++的min 和 max 特性
3.1、C++ 11中的 std::initializer_list
在c++ 11中,std::min和std::max出现了新的重载。前面的代码只使用了两个(引用)值,为什么只局限于此呢?范围的情况由std::min_element和std::max_element覆盖,但是取不属于范围的几个值中的最小值也是有意义的。
在C++11中,std::min
和std::max
出现了新的重载函数。之前的函数只接受两个值(引用),为什么不扩展到接受更多值呢?对于一个范围的值,std::min_element
和std::max_element
可以处理,但是对于不在一个范围内的多个值的最小值也是有意义的。
新的重载通过接受std::initializer_list
来实现这一点:
template<typename T>
T min(std::initializer_list<T> ilist);
它可以这样用:
int minValue = std::min({4, 1, 5, 5, 8, 3, 7});
这个例子直接使用明文数字,std::initializer_list
中也接受变量。
注意,与std::min
和std::max
的基本重载相反,返回的是较小(或较大)值的副本,而不再是引用。实际上,在基本重载中,引用绑定到实参,这对于初始化列表没有意义。
3.2、C++ 14中的 constexpr
C++ 14为目前看到的所有算法带来了新的constexpr
重载。例如:
template<typename ForwardIt>
constexpr ForwardIt max_element(ForwardIt first, ForwardIt last);
这有两个显著的影响:
- 所有与
min
和max
相关的算法都可用于计算编译时值,可用于模板参数。 - 编译器能够进行令人印象深刻的优化,并且在某些情况下能够完全消除与查找最小值或最大值相关的代码。
四、std::max的一个bug
std::max
中有一个bug,并且这个bug正变得越来越明显。有人可能会想,这怎么可能呢?这样一个简单而广泛使用的函数怎么会有bug呢?
在特定情况下,当两个元素比较相等时(通过使用operator<
或自定义比较器),就会发生这种情况。然后min
返回对第一个的引用,这是可以的,但是max
也返回对第一个的引用。这很奇怪。因为期望是在一对元素中,最大值总是小于最小值。
这个 bug 的出现是因为 std::max
函数的实现使用了 operator<
来进行比较。当两个元素相等时,operator<
返回 false
,而 std::max
会错误地将第一个元素视为最大值。
用一个示例来验证这个 bug:
#include <iostream>
#include <algorithm>
int main() {
int a = 5;
int b = 5;
int& maxRef = std::max(a, b);
// 修改 a 的值
a = 10;
std::cout << "Max value: " << maxRef << std::endl;
return 0;
}
预期输出应该是 “Max value: 10”,因为在代码中将 a
的值修改为 10。然而,由于 std::max
的 bug,实际输出会是 “Max value: 5”,因为 maxRef
仍然指向初始值较小的元素。
为了解决这个 bug,可以使用 std::max
的另一个重载版本,接受一个自定义的比较器作为参数,或者使用条件表达式 a > b ? a : b
来获取正确的最大值。
C++ 11中引入的新算法std::minmax
纠正了这一点。minmax
返回一个包含它接收到的两个值的最小值和最大值的值对(pair
)。如果这些值相等,那么最小值是第一个,最大值是第二个。
template<typename T>
std::pair<const T&,const T&> minmax(const T& a, const T& b);
std::minmax
具有std::min
和std::max
的所有技术特性:
- 返回引用;
- 自定义比较;
- 对
std::minmax_element
、initializer_list
和constexpr
的支持。
五、总结
那么,读完这篇文章,还认为最小值和最大值真的那么简单吗?
最小值和最大值函数看似简单,但在实际使用中还是需要注意一些细节问题。通过深入了解C++标准库中相关函数的特性和注意事项,可以更好地掌握它们的使用方法,提高编程效率和代码质量。