目录
[4.标准库 std::accumulate](#4.标准库 std::accumulate)
1.背景
最新在看别人的代码,看到一个函数,函数的目的是求std::vector<double>的所有值的绝对值的平均值,代码如下:
cpp
double absAverage(const std::vector<double>& vecValues)
{
double sum = 0.0;
if (vecValues.size() <= 0)
return 0.0;
for (auto& it : vecValues) {
sum += std::abs(it);
}
return sum / vecValues.size();
}
功能上实现看似没有什么问题,但是深层次去思考,至少我知道的就有个最大的问题:
通用性严重不足
- 硬编码容器类型 只能接收
std::vector<double>,无法适配std::array、std::deque、原生 C 数组、std::initializer_list等其他数值序列,每换一种容器就要重复写一份代码。 - 硬编码元素类型 仅支持
double,int、float、long long等所有其他算术类型都无法使用,类型扩展成本极高。
那么怎么才能做到通用性呢?下面就来讲讲。
2.C++模板函数
3.抽象容器类型
3.1.普通容器
首先对于C++的标准容器有std::vector/std::array/std::list/std::deque等等,Qt的容器QVector/QList等,它们都有自己的value_type, pointer等,std::vector如下定义:
cpp
_EXPORT_STD template <class _Ty, class _Alloc = allocator<_Ty>>
class vector { // varying size array of values
private:
template <class>
friend class _Vb_val;
friend _Tidy_guard<vector>;
using _Alty = _Rebind_alloc_t<_Alloc, _Ty>;
using _Alty_traits = allocator_traits<_Alty>;
public:
static_assert(!_ENFORCE_MATCHING_ALLOCATORS || is_same_v<_Ty, typename _Alloc::value_type>,
_MISMATCHED_ALLOCATOR_MESSAGE("vector<T, Allocator>", "T"));
static_assert(is_object_v<_Ty>, "The C++ Standard forbids containers of non-object types "
"because of [container.requirements].");
using value_type = _Ty;
using allocator_type = _Alloc;
using pointer = typename _Alty_traits::pointer;
using const_pointer = typename _Alty_traits::const_pointer;
using reference = _Ty&;
using const_reference = const _Ty&;
using size_type = typename _Alty_traits::size_type;
using difference_type = typename _Alty_traits::difference_type;
。。。
};
那么就可以这样设计:
cpp
template <typename Container,
typename Enabled = std::enable_if<std::is_arithmetic_v<typename std::decay_t<Container>::value_type>> >
double absAverage(Container&& container) noexcept
{
using RawContainer = std::decay_t<Container>;
using ValType = typename RawContainer::value_type;
// 替换concept约束
static_assert(std::is_arithmetic<ValType>::value, "容器元素必须是内置算术基础类型");
if (container.empty())
return 0.0;
double sum = 0.0;
for (auto& it : vecValues) {
sum += std::abs(it);
}
return sum / container.size();
}
用std::is_arithmetic_v、std::enable_if 限制容器的数据类型是标量。函数参数声明为Container&&的作用是:万能转发引用 T&&(模板专属,自动推导左 / 右值)
- 传入左值:
T推导为Type&→ 参数变成Type& &&,折叠后 =Type&,我们手动加const转为const Type&; - 传入右值:
T推导为Type→ 参数变成Type&&,直接接管右值。
| 传入参数 | 推导 T | 形参 Container&& 折叠结果 | 处理逻辑 |
|---|---|---|---|
左值 vector<int> v |
vector<int>& |
vector<int>& && → vector<int>& |
函数内部只读,等价 const& 语义,不修改原数据 |
右值 vector<int>{} |
vector<int> |
vector<int>&& |
右值引用,直接接管临时对象 |
调用示例:
cpp
std::vector<int> vecInts = {1, 2, 100, 333, 444, -1};
auto sumOfInt = absAverage(vecInts);
std::vector<double> vecDoubles = { 1, 2, 100, 333, 444, 2};
auto sumOfDouble = absAverage(vecDoubles);
3.2.C原生数组
上面的函数发现对原生数组没有用,因此特例化上面函数,代码如下:
cpp
template <typename T, size_t N>
double absAverage(T(&container)[N]) noexcept
{
double sum = 0.0;
for (int i = 0; i < N; i++) {
sum += std::abs(container[i]);
}
return sum / N;
}
调用示例:
cpp
int a[] = {11, 33,23333, 435, 2211212, 3333, 5555,6666,432,111,233,3,3,33,34 };
auto sumOfInt1 = absAverage(a);
3.3.std::initializer_list
cpp
template <typename T>
double absAverage(std::initializer_list<T> container) noexcept
{
if (container.size() <= 0)
return 0.0;
double sum = 0.0;
for (auto& it : vecValues) {
sum += std::abs(it);
}
return sum / container.size();
}
调用示例:
cpp
auto sumOfInt2 = absAverage({11,22,33,44,56,777,88,99});
4.标准库 std::accumulate
头文件:<numeric>
定义:
cpp
// 基础版本
template< class InputIt, class T >
T accumulate( InputIt first, InputIt last, T init );
// 自定义二元操作
template< class InputIt, class T, class BinaryOp >
T accumulate( InputIt first, InputIt last, T init, BinaryOp op );
等价逻辑伪代码:
cpp
auto result = init;
for (; first != last; ++first) {
result = op(result, *first);
}
return result;
于是上面的循环代码可以修改为:
cpp
// 累加所有元素的绝对值,初始值0.0保证浮点精度
double sumAbs = std::accumulate(
std::cbegin(cont),
std::cend(cont),
0.0,
[](double accum, ValueType val)
{
return accum + std::abs(val);
}
);
5.进一步优化
不管C++标准容器、Qt容器还是C原生数组,都是容器,那么3.1和3.2就可以合在一起,利用std::begin、std::end获取迭代器类型,再利用std::iterator_traits获取value_type, std::iterator_traits的定义如下:
cpp
_EXPORT_STD template <class _Ty>
struct iterator_traits : _Iterator_traits_base<_Ty> {
using _From_primary = iterator_traits;
};
template <class _Ty>
requires is_object_v<_Ty>
struct iterator_traits<_Ty*> {
using iterator_concept = contiguous_iterator_tag;
using iterator_category = random_access_iterator_tag;
using value_type = remove_cv_t<_Ty>;
using difference_type = ptrdiff_t;
using pointer = _Ty*;
using reference = _Ty&;
};
于是代码优化为:
cpp
// 通用模板:支持任意可迭代序列 + 算术类型元素约束
template<typename Container>
auto absAverageEx(Container&& cont)
-> std::enable_if_t<
std::is_arithmetic_v<
typename std::iterator_traits<decltype(std::begin(cont))>::value_type>,
double
>
{
// 提取元素真实类型
using ValueType = typename std::iterator_traits<decltype(std::begin(cont))>::value_type;
// 空容器安全处理,避免除以0
const auto size = std::size(cont);
if (size == 0)
{
return 0.0;
}
// 累加所有元素的绝对值,初始值0.0保证浮点精度
double sumAbs = std::accumulate(
std::cbegin(cont),
std::cend(cont),
0.0,
[](double accum, ValueType val)
{
return accum + std::abs(val);
}
);
return sumAbs / static_cast<double>(size);
}
C++20 Concept 简化版:
语法更简洁,报错信息更直观,替代 enable_if:
cpp
#include <concepts>
// 定义算术容器概念
template<typename Container>
concept ArithmeticContainer = requires(Container c) {
std::begin(c);
std::end(c);
requires std::is_arithmetic_v<
typename std::iterator_traits<decltype(std::begin(c))>::value_type
>;
};
template<ArithmeticContainer Container>
double absAverage(Container&& cont)
{
using ValueType = typename std::iterator_traits<decltype(std::begin(cont))>::value_type;
const auto size = std::size(cont);
if (size == 0) return 0.0;
double sumAbs = std::accumulate(
std::cbegin(cont), std::cend(cont), 0.0,
[](double accum, ValueType val) {
return accum + std::abs(val);
}
);
return sumAbs / static_cast<double>(size);
}
C++11 兼容版(无 std::size、无 _v 后缀)
如果需要兼容旧编译器,做两处替换:
- 元素数量用
std::distance计算(非随机访问迭代器为 O (n) 复杂度); is_arithmetic_v<T>替换为is_arithmetic<T>::value。
cpp
template<typename Container>
typename std::enable_if<
std::is_arithmetic<
typename std::iterator_traits<decltype(std::begin(cont))>::value_type
>::value,
double
>::type
absAverage(Container&& cont)
{
using ValueType = typename std::iterator_traits<decltype(std::begin(cont))>::value_type;
const auto size = std::distance(std::begin(cont), std::end(cont));
if (size == 0) return 0.0;
double sumAbs = std::accumulate(
std::begin(cont), std::end(cont), 0.0,
[](double accum, ValueType val) {
return accum + std::abs(val);
}
);
return sumAbs / static_cast<double>(size);
}
对于{11,22,33}不支持的核心原因:
{11,22,33,...} 是花括号初始化列表(braced-init-list),它不是一个带有确定类型的 C++ 对象,在模板参数推导中属于「非推导语境」:
- 编译器无法自动把裸花括号列表推导为
std::initializer_list<int>,因此模板参数Container推导失败,直接报错; - 原函数的转发引用
Container&&要求入参有明确类型,裸花括号列表不满足推导规则。
解决方案:新增 std::initializer_list 专属重载
只需要加一个接收 std::initializer_list<T> 的重载函数,专门承接花括号传参,内部复用原有通用逻辑,不需要重复写计算代码。
完整可运行代码(兼容所有容器 + 花括号列表):
cpp
#include <iostream>
#include <numeric>
#include <cmath>
#include <type_traits>
#include <iterator>
#include <vector>
#include <array>
#include <list>
#include <initializer_list>
// 通用版本:支持所有带迭代器的容器(vector/array/list/原生数组等)
template<typename Container>
auto absAverage(Container&& cont)
-> std::enable_if_t<
std::is_arithmetic_v<
typename std::iterator_traits<decltype(std::begin(cont))>::value_type
>,
double
>
{
using ValueType = typename std::iterator_traits<decltype(std::begin(cont))>::value_type;
const auto size = std::size(cont);
if (size == 0)
{
return 0.0;
}
double sumAbs = std::accumulate(
std::cbegin(cont),
std::cend(cont),
0.0,
[](double accum, ValueType val)
{
return accum + std::abs(val);
}
);
return sumAbs / static_cast<double>(size);
}
// 新增重载:专门支持花括号初始化列表 {a,b,c,...}
template<typename T>
std::enable_if_t<std::is_arithmetic_v<T>, double>
absAverage(std::initializer_list<T> list)
{
// 直接复用通用版本的计算逻辑,代码不重复
return absAverage(list);
}
int main()
{
// 1. 花括号列表直接调用(你要的写法)
double sumOfInt2 = absAverage({11,22,33,44,56,777,88,99});
std::cout << "花括号列表均值: " << sumOfInt2 << "\n";
// 2. 原有容器用法全部保留
std::vector<int> vec = {-1, -2, 3, -4, 5};
std::cout << "vector均值: " << absAverage(vec) << "\n";
std::array<int, 4> arr = {-10, 20, -30, 40};
std::cout << "array均值: " << absAverage(arr) << "\n";
int c_arr[] = {1, -2, 3, -4};
std::cout << "原生数组均值: " << absAverage(c_arr) << "\n";
return 0;
}
关键说明:
1.重载匹配逻辑
调用 absAverage({11,22,...}) 时,编译器会优先匹配 std::initializer_list<T> 重载,自动推导 T=int,再转发给通用容器版本执行计算,逻辑完全统一。
2.类型一致性要求
花括号内的元素类型必须一致:
- ✅ 合法:
absAverage({1,2,3})(全 int)、absAverage({1.0, 2.5, 3.0})(全 double) - ❌ 报错:
absAverage({1, 2.5, 3})(int+double 混合,无法推导 T) 混合类型时可显式指定类型:absAverage<double>({1, 2.5, 3})
3.性能开销
std::initializer_list 内部只保存首尾指针,值传递成本极低,和引用传递几乎没有差别,不需要担心拷贝开销。
4.C++11 兼容调整
如果编译器不支持 std::size(C++17),把通用版本里的 std::size(cont) 替换为 std::distance(std::begin(cont), std::end(cont)) 即可。