Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

span-compat

CI

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::span is std::span.
  • At C++17, the standard library has no std::span. Then span_compat::span is 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.

Purpose

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 selection rule

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).

One span type per build

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:

  • AUTO is 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.

Usage

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));
}

API subset

The own implementation supplies this subset of std::span:

  • span<T> (dynamic extent) and span<T, N> (static extent), and span_compat::dynamic_extent.
  • The member types element_type, value_type, size_type, difference_type, pointer, const_pointer, reference, const_reference and iterator, and the constant extent.
  • Construction:
    • default (empty, only for a dynamic extent or extent 0);
    • pointer and count;
    • C array;
    • std::array and const std::array;
    • a container with data(), size(), begin() and end(), for example std::vector, std::string or etl::vector;
    • another span, with a conversion from T to const 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() and subspan(), 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 const elements.
  • 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.
  • nullptr or a literal 0 is 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().

Bounds

Like std::span, the own implementation does no bounds check. Your code must make sure of these conditions:

  • operator[]: the index is less than size().
  • front() and back(): the span is not empty.
  • first(count) and last(count): count is not more than size().
  • subspan(offset, count): offset is not more than size(). count is dynamic_extent or not more than size() - 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.

Build and test

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-failure

The 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.sh compiles the header with arm-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.sh installs the library into a temporary prefix. Then it builds a consumer with find_package(span-compat) at C++17 and C++23. It also installs with the option OFF. Then the consumer at C++23 must get the own implementation. The install must also contain LICENSE.
  • tests/check-subproject.sh builds a superproject that gets span-compat with FetchContent. A C++17 library and a C++23 application in it link span-compat with the option OFF. Both must get the own implementation.
  • The target clangformat-check checks the format of include/ and tests/ with clang-format 20. The target clangformat formats 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.

License

span-compat is available under the MIT license. See LICENSE. The install puts LICENSE into share/doc/span-compat.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages