C++设计一个函数来求容器元素绝对值之和的平均值

目录

1.背景

2.C++模板函数

3.抽象容器类型

3.1.普通容器

3.2.C原生数组

3.3.std::initializer_list

[4.标准库 std::accumulate](#4.标准库 std::accumulate)

5.进一步优化


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::arraystd::deque、原生 C 数组、std::initializer_list 等其他数值序列,每换一种容器就要重复写一份代码。
  • 硬编码元素类型 仅支持 doubleintfloatlong long 等所有其他算术类型都无法使用,类型扩展成本极高。

那么怎么才能做到通用性呢?下面就来讲讲。

2.C++模板函数

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

C++之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++标准库之std::begin、std::end、std::pre和std::next

不管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)) 即可。

相关推荐
王老师青少年编程2 小时前
csp信奥赛C++高频考点专项训练:【排序算法】案例9:成绩排序
c++·排序算法·csp·高频考点·信奥赛·成绩排序
程序喵大人3 小时前
【C++进阶】STL容器与迭代器 - 05 map 和 set 为什么按键保持有序
开发语言·c++·容器·迭代器·stl
库克克3 小时前
【C++】 unordered_map 与unordered_set
开发语言·c++
李迟5 小时前
一种轻量级C++ CSV文件读写库的实现方案
开发语言·c++
学无止境_永不停歇5 小时前
9. 缓冲区
linux·服务器·c++
古道青阳5 小时前
Duilib 技术全景剖析
c++·windows
Rabitebla6 小时前
C++ 内存管理全面复习:从内存分布到 operator new/delete
java·c语言·开发语言·c++·算法·leetcode
SilentSlot6 小时前
【C/C++】手写 DPDK 协议栈(八):用五元组 Hash 加速连接定位
c语言·c++·哈希算法
jing.wang_20257 小时前
NVIDIA CUDA C++编程环境搭建--Windows + Ubuntu 22.04
c++·windows·ubuntu·gpu算力