I'm writing a C++ library that has to externally expose a C API. While doing it, I find myself questioning what is the better way to wrap the C idiomatic pair of (pointer, size)
. I find myself having a hard time choosing between std::ranges::contiguous_range
and std::span
.
Where I am right now is that, while contiguous_range
is more generic, span
seems more natural for this use case. The answer here says that span
erases the type of the container, or maybe makes it irrelevant. Which is good for me, as this is embedded and code size matters. Also, I can not use anything that would not compile under GCC 10 (so most, if not all, of C++20 is okay).
Are there any pros or cons to using one over the other in my use case?
A small sample of the two options - both working.
Option 1 - the one I initially went with - is to use std::ranges::contiguous_range
, like this:
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())
);
}
Option 2 is to use std::span
, like this:
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());
}