span-compat is a header-only C++ library. It gives one span type, span_compat::span, at C++17 and later:
- At C++20 and later,
span_compat::spanisstd::span. - At C++17, the standard library has no
std::span. Thenspan_compat::spanis an own implementation with the same interface subset.
The library has no dependencies. It uses no heap, no exceptions and no RTTI (run-time type information). Thus it is
safe for firmware that builds with -fno-exceptions -fno-rtti.
Some libraries, for example system-stm32 and libArT, must build at C++17. The firmware that uses them builds at C++23. The public interface of these libraries uses spans. span-compat is the single source of the span type for all of them.
The macro SPAN_COMPAT_USE_STD_SPAN selects the type:
SPAN_COMPAT_USE_STD_SPAN |
span_compat::span |
|---|---|
Not defined, __cpp_lib_span defined (C++20 and later) |
std::span |
Not defined, __cpp_lib_span not defined (C++17) |
the own implementation |
0 |
the own implementation, also at C++20 and later |
1 |
std::span; an #error stops the build if __cpp_lib_span is not defined |
The header reads __cpp_lib_span from <version>. The GitLab CI pipeline checks arm-none-eabi-g++ 14.3 (Arm GNU
Toolchain 14.3.Rel1). A local check also passes with 15.2. With both versions, the macro is absent at C++17. It is
present at C++20 and C++23. The GitHub Actions workflow builds with 15.3 (Arm GNU Toolchain 15.3.Rel2).
All libraries and the application in one build must see the same span type. Thus all translation units must see the
same value of SPAN_COMPAT_USE_STD_SPAN.
If two parts of a program see different values, then the program contains two different span types. Then a call
between the two parts can fail to link. For example, a function with a std::span parameter does not link with a
call that uses the own span. Inline functions and templates with different definitions break the one definition rule
(ODR). The result is undefined behaviour.
Some data structures do not cause a link error. A class with a span member, or an inline function in a shared header, compiles in both parts with a different span type. The two layouts also differ. The linker does not find this error.
Thus set the type once, for the full build, with the CMake option SPAN_COMPAT_USE_STD_SPAN:
| CMake option value | Effect |
|---|---|
AUTO (default) |
The target sets no macro. Each translation unit selects the type by __cpp_lib_span. |
ON |
The target defines SPAN_COMPAT_USE_STD_SPAN=1 and requires C++20 (cxx_std_20). |
OFF |
The target defines SPAN_COMPAT_USE_STD_SPAN=0: the own implementation at all standards. |
The option sets the macro on the target span-compat::span-compat. Thus all targets that link it get the same value.
The installed package keeps the value, so a find_package consumer also gets it.
Obey these rules:
AUTOis correct only if all targets of the build use the same C++ standard.- If a C++17 library is part of a C++20 or C++23 build, set the option to
OFF. - Set the option in the superproject, before you add span-compat. Do not define the macro on one library only.
- A library that uses span-compat (for example libArT) must set this CMake option. It must not set its own compile definition.
FetchContent adds the target span-compat::span-compat only once. Thus libArT and system-stm32 share one target, and
the option applies to both.
Add the library with CMake. You can use add_subdirectory, FetchContent or an installed package:
# Optional: the span type of the full build (see "One span type per build")
set(SPAN_COMPAT_USE_STD_SPAN OFF CACHE STRING "")
# Source tree: FetchContent or add_subdirectory
include(FetchContent)
FetchContent_Declare(span_compat
GIT_REPOSITORY https://github.com/armint/span-compat.git
GIT_TAG main)
FetchContent_MakeAvailable(span_compat)
# Installed package
find_package(span-compat 1 REQUIRED CONFIG)
target_link_libraries(my-target PUBLIC span-compat::span-compat)The target span-compat::span-compat is an INTERFACE target. It sets the include directory and the cxx_std_17
minimum. With the option ON or OFF, it also sets SPAN_COMPAT_USE_STD_SPAN.
Include the header and use the type:
#include <span-compat/span.h>
#include <array>
#include <cstdint>
void send(span_compat::span<const std::uint8_t> data);
void example() {
std::array<std::uint8_t, 8> buffer = {};
send(buffer);
send(span_compat::span(buffer).subspan(2, 4));
}The own implementation supplies this subset of std::span:
span<T>(dynamic extent) andspan<T, N>(static extent), andspan_compat::dynamic_extent.- The member types
element_type,value_type,size_type,difference_type,pointer,const_pointer,reference,const_referenceanditerator, and the constantextent. - Construction:
- default (empty, only for a dynamic extent or extent 0);
- pointer and count;
- C array;
std::arrayandconst std::array;- a container with
data(),size(),begin()andend(), for examplestd::vector,std::stringoretl::vector; - another span, with a conversion from
Ttoconst T, and between static and dynamic extent.
- Class template argument deduction (CTAD) for the constructions above.
data(),size(),size_bytes(),empty().operator[],front(),back(),begin(),end().- The iterator is a random access iterator class, not a pointer, as in libstdc++. At C++20 and later, it is also a contiguous iterator.
first(),last()andsubspan(), each with a template argument (static extent) and with a function argument (dynamic extent).
The construction rules are the same as for std::span. Code that std::span rejects is also rejected at C++17:
- A conversion only adds
const. A span of a base class does not accept the elements of a derived class. - A temporary container is accepted only for a span of
constelements. - A conversion to a static extent from a dynamic size is
explicit. Examples are pointer and count, a container, or a span with a dynamic extent. - An array or a static span with a different extent does not convert.
nullptror a literal0is not a pointer to elements. The pointer and count construction rejects them.
The own implementation is a standard-layout type, as std::span. At C++20 and later (with
SPAN_COMPAT_USE_STD_SPAN=0), it is also a view and a borrowed range for std::ranges. Thus it converts to
std::span, and a std::span converts to it.
The own implementation does not supply rbegin(), rend(), as_bytes() and as_writable_bytes().
Like std::span, the own implementation does no bounds check. Your code must make sure of these conditions:
operator[]: the index is less thansize().front()andback(): the span is not empty.first(count)andlast(count):countis not more thansize().subspan(offset, count):offsetis not more thansize().countisdynamic_extentor not more thansize() - offset.- Construction with a static extent from a dynamic size: the size is equal to the extent.
A wrong value causes undefined behaviour. The template forms first<N>(), last<N>() and subspan<Offset, Count>()
on a static extent check the bounds at compile time.
The tests use doctest. CMake downloads it with FetchContent. Build and run the tests on the host:
cmake -B build
cmake --build build -j
ctest --test-dir build --output-on-failureThe build makes three test binaries from the same test sources:
| Binary | Standard | Type |
|---|---|---|
span-compat-tests-cpp17 |
C++17 | own implementation |
span-compat-tests-cpp23 |
C++23 | std::span |
span-compat-tests-cpp23-own |
C++23 | own implementation (SPAN_COMPAT_USE_STD_SPAN=0) |
Each binary checks the type with static_assert. Thus the own implementation and std::span pass the same tests.
Other checks:
tests/check-firmware-build.shcompiles the header witharm-none-eabi-g++at C++17 and C++23. It uses-fno-exceptions -fno-rtti -Wall -Wextra -Wpedantic -Werror. The object file must not use the exception or RTTI runtime.tests/check-install.shinstalls the library into a temporary prefix. Then it builds a consumer withfind_package(span-compat)at C++17 and C++23. It also installs with the optionOFF. Then the consumer at C++23 must get the own implementation. The install must also containLICENSE.tests/check-subproject.shbuilds a superproject that gets span-compat withFetchContent. A C++17 library and a C++23 application in it link span-compat with the optionOFF. Both must get the own implementation.- The target
clangformat-checkchecks the format ofinclude/andtests/with clang-format 20. The targetclangformatformats the files.
The CMake options are:
| Option | Default | Function |
|---|---|---|
SPAN_COMPAT_BUILD_TESTS |
ON as top-level project, else OFF |
Build the test binaries |
SPAN_COMPAT_INSTALL |
ON as top-level project, else OFF |
Generate the install rules |
SPAN_COMPAT_USE_STD_SPAN |
AUTO |
The span type of the build: AUTO, ON or OFF (see "One span type per build") |
The tests require SPAN_COMPAT_USE_STD_SPAN=AUTO, because each test binary selects its own type.
The GitLab CI pipeline (.gitlab-ci.yml) runs all of these checks. The GitHub Actions workflow
(.github/workflows/ci.yml) runs the same checks in the public image
ghcr.io/armint/firmware-build-image.
span-compat is available under the MIT license. See LICENSE. The install puts LICENSE into
share/doc/span-compat.