在C++中包装C数组:std::span与std::ranges::contiguous_range如何选择?
在对外暴露C API的C++库中,std::ranges::contiguous_range与std::span的选型对比
我正在开发一个对外暴露C API的C库,需要包装C语言惯用的(指针, 大小)对,目前在std::ranges::contiguous_range和std::span之间难以抉择。已知我只能使用GCC 10支持的特性(几乎所有C20特性均可使用),且开发场景为嵌入式环境,对代码体积有较高要求。
方案1:std::ranges::contiguous_range 方案
优点
- 通用性拉满:支持所有满足
contiguous_range约束的uint8_t类型容器(如std::array、std::vector、std::string_view、原生数组等),用户调用时无需显式转换,直接传入容器即可。 - 保留类型信息:模板会针对不同容器类型实例化,若后续需要针对特定容器做定制化优化,保留了扩展潜力。
缺点
- 代码体积膨胀:每个传入的容器类型都会触发一次模板实例化,生成独立的函数版本,在嵌入式场景下会显著增加二进制体积。
- 代码复杂度高:需要自定义
concept来约束类型,获取大小和指针时还要调用std::ranges::size和std::to_address,不如span直观简洁。 - 用户理解成本高:
contiguous_range的概念对普通用户来说不如span直观,需要额外理解C++20 range的相关特性。
示例代码:
template<typename T> concept contiguous_uint8_range = std::ranges::contiguous_range<T> && std::is_same_v<uint8_t, std::ranges::range_value_t<T>>; [[nodiscard]] static inline int read( const struct i2c_controller* controller, uint8_t addr_7b, contiguous_uint8_range auto output_buffer ) { if (controller->read == nullptr) return I2C_ERROR_NOT_SUPPORTED; return controller->read( controller->context, addr_7b, std::ranges::size(output_buffer), std::to_address(output_buffer.begin()) ); }
方案2:std::span 方案
优点
- 代码体积可控:
span会擦除容器的具体类型,仅根据扩展范围(动态/固定)实例化模板,大幅减少二进制体积,完美适配嵌入式场景。 - 接口简洁直观:
span本身就是为包装(指针, 大小)对设计的,直接通过size()和data()就能获取C API需要的参数,代码更简洁易读。 - 兼容性优异:可以直接接收原生数组、
std::array、std::vector等连续容器,也支持显式构造,用户调用成本极低。 - GCC 10完美支持:
std::span是C++20标准特性,GCC 10已完全实现,无需担心兼容性问题。
缺点
- 自定义range需显式转换:如果用户传入自定义的连续range,需要显式构造
std::span对象才能传入,而contiguous_range方案可以直接接收。 - 固定大小span有编译约束:使用固定扩展范围的
span时,若传入容器大小不匹配会直接编译报错,需要用户注意参数匹配。
示例代码:
template<std::size_t Extent = std::dynamic_extent> using u8_span = std::span<uint8_t, Extent>; template<std::size_t Extent = std::dynamic_extent> [[nodiscard]] static inline int read( const struct i2c_controller* controller, uint8_t addr_7b, u8_span<Extent> output_buffer ) { if (controller->read == nullptr) return I2C_ERROR_NOT_SUPPORTED; return controller->read(controller->context, addr_7b, output_buffer.size(), output_buffer.data()); }
选型建议
结合你的嵌入式场景和代码体积要求,优先选择std::span方案:它完全贴合C API的(指针,大小)调用模式,代码体积更可控,接口也更简洁。只有当你需要最大化通用性,且能接受代码体积增加时,才考虑contiguous_range方案。
内容的提问来源于stack exchange,提问作者jaskij
相关产品推荐
相关产品推荐

