xo-flatstring: + README + docs + test coverage

This commit is contained in:
Roland Conybeare 2024-04-16 17:25:14 -04:00
commit a958217c38
26 changed files with 4161 additions and 156 deletions

View file

@ -1,8 +1,8 @@
# xo-stringliteral/CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
cmake_minimum_required(VERSION 3.25)
project(xo_stringliteral VERSION 1.0)
project(xo_flatstring VERSION 1.0)
enable_language(CXX)
# common XO cmake macros (see proj/xo-cmake)
@ -12,17 +12,37 @@ include(cmake/xo-bootstrap-macros.cmake)
# unit test setup
enable_testing()
# activate code coverage for all executables + libraries (when configured with -DCODE_COVERAGE=ON)
add_code_coverage()
# 1. assuming that /nix/store/ prefixes .hpp files belonging to gcc, catch2 etc.
# we're not interested in code coverage for these sources.
# 2. exclude the utest/ subdir, we don't need coverage on the unit tests themselves;
# rather, want coverage on the code that the unit tests exercise.
# ----------------------------------------------------------------
# cmake -DCMAKE_BUILD_TYPE=coverage
if (NOT DEFINED PROJECT_CXX_FLAGS_COVERAGE)
# note: for clang would use -fprofile-instr-generate -fcoverage-mapping here instead and also at link time
set(PROJECT_CXX_FLAGS_COVERAGE ${PROJECT_CXX_FLAGS} -ggdb -Og -fprofile-arcs -ftest-coverage
CACHE STRING "coverage c++ compiler flags")
endif()
message("-- PROJECT_CXX_FLAGS_COVERAGE: coverage c++ flags are [${PROJECT_CXX_FLAGS_COVERAGE}]")
add_compile_options("$<$<CONFIG:COVERAGE>:${PROJECT_CXX_FLAGS_COVERAGE}>")
# when -DCMAKE_BUILD_TYPE=coverage, link executables with gcov
link_libraries("$<$<CONFIG:COVERAGE>:gcov>")
find_program(LCOV_EXECUTABLE NAMES lcov)
find_program(GENHTML_EXECUTABLE NAMES genhtml)
# with coverage build:
# 1. invoke instrumented executables for which you want coverage:
# (cd path/to/build && ctest)
# 2. post-process low-level coverage data
# (path/to/build/gen-ccov)
# 3. point browser to generated html data
# file:///path/to/build/ccov/html/index.html
#
# NOTE: this seems to work only with the 'ccov-all' target. In particular, doesn't seem to do anything with the 'ccov' target
#
add_code_coverage_all_targets(EXCLUDE /nix/store/* ${PROJECT_SOURCE_DIR}/utest/* ${PROJECT_BINARY_DIR}/local/* ${PROJECT_SOURCE_DIR}/repo/*)
configure_file(
${PROJECT_SOURCE_DIR}/cmake/gen-ccov.in
${PROJECT_BINARY_DIR}/gen-ccov)
file(CHMOD ${PROJECT_BINARY_DIR}/gen-ccov PERMISSIONS OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
# ----------------------------------------------------------------
# c++ settings
@ -37,8 +57,8 @@ xo_toplevel_compile_options()
# ----------------------------------------------------------------
add_subdirectory(example)
#add_subdirectory(utest)
#add_subdirectory(docs)
add_subdirectory(utest)
add_subdirectory(docs)
# ----------------------------------------------------------------
# provide find_package() support for projects using this library

59
README.md Normal file
View file

@ -0,0 +1,59 @@
# flatstring library
Fixed-length no-allocation string implementation.
Features:
- char array representation with maximum size set at compile time.
- compile time construction from char array and string concatenation
- pointer-free implementation, instances can be used as template arguments
- To the extent practical, provides the same api as `std::string`
Limitations:
- requires c++20
- not resizable.
- does not support wide characters.
## Getting started
### build + install
```
$ cd xo-flatstring
$ mkdir .build
$ PREFIX=/usr/local # for example
$ cmake -DCMAKE_INSTALL_PREFIX=${PREFIX} -B .build
$ cmake --build .build
$ cmake --install .build
```
### build documentation
```
$ cd xo-flatstring
$ cmake --build .build -- docs
```
When complete, point local browser to `xo-flatstring/.build/docs/sphinx/index.html`
### build with test coverage
```
$ cd xo-flatstring
$ mkdir .build-ccov
$ cmake -DCMAKE_INSTALL_PREFIX=$PREFIX -DCODE_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug -B .build-ccov
$ cmake --build .build-ccov
```
run coverage-enabled unit tests
```
$ (cd .build-ccov && ctest)
```
generate html+text coverage report
```
$ .build-ccov/gen-ccov
```
browse to `.build-ccov/ccov/html/index.html`
### LSP support
```
$ cd xo-flatstring
$ ln -s .build/compile_commands.json
```

20
cmake/gen-ccov.in Normal file
View file

@ -0,0 +1,20 @@
#!/usr/bin/env bash
srcdir=@PROJECT_SOURCE_DIR@
builddir=@PROJECT_BINARY_DIR@
lcov=@LCOV_EXECUTABLE@
genhtml=@GENHTML_EXECUTABLE@
if [[ $lcov == "LCOV_EXECUTABLE-NOTFOUND" ]]; then
echo "gen-ccov: lcov executable not found"
exit 1
fi
if [[ $genhtml == "GENHTML_EXECUTABLE-NOTFOUND" ]]; then
echo "gen-ccov: genhtml executable not found"
exit 1
fi
mkdir $builddir/ccov
$srcdir/cmake/lcov-harness $srcdir $builddir $builddir/ccov/out $lcov $genhtml

114
cmake/lcov-harness Executable file
View file

@ -0,0 +1,114 @@
#!/usr/bin/env bash
srcdir=$1
builddir=$2
outputstem=$3
lcov=$4
genhtml=$5
if [[ -z "${srcdir}" ]]; then
echo "lcov-harness: expected non-empty srcdir"
exit 1
fi
if [[ -z ${builddir} ]]; then
echo "lcov-harness: expected non-empty builddir"
exit 1
fi
if [[ -z ${outputstem} ]]; then
echo "lcov-harness: expected non-empty outputstem"
exit 1
fi
if [[ -z ${lcov} ]]; then
echo "lcov-harness: exepcted non-empty lcov"
exit 1
fi
if [[ -z ${genhtml} ]]; then
echo "lcov-harness: expected non-empty genhtml"
exit 1
fi
# directory stems for location of {.gcda, gcno} coverage information,
#
# if we have source tree:
#
# ${srcdir}
# +- foo
# | \- foo.cpp
# \- bar
# \- quux
# +- quux.cpp
# \- quux_main.cpp
#
# then we expect build tree:
#
# ${builddir}
# +- foo
# | \- CMakeFiles
# | \- foo_target.dir
# | +- foo.cpp.gcda
# | \- foo.cpp.gcno
# +- bar
# \- quux
# \- CMakeFiles
# \- target4quux.dir
# +- quux.cpp.gcda
# +- quux.cpp.gcno
# +- quux_main.cpp.gcda
# \- quux_main.cpp.gcno
#
# in which case will have cmd_body:
#
# ${primarydirs}
# ./foo/CMakeFiles/foo_target.dir
# ./bar/quux/CMakeFiles/target4quux.dir
#
# here foo_target, quux_target are whatever build is using for corresponding cmake target names.
#
# We want to invoke lcov like:
#
# lcov --capture \
# --output ${builddir}/ccov \
# --exclude /utest/ \
# --base-directory ${srcdir}/foo --directory ${builddir}/foo/CMakeFiles/foo_target.dir \
# --base-directory ${srcdir}/bar/quux --directory ${builddir}/bar/quux/CMakeFiles/target4quux.dir
#
primarydirs=$(cd ${builddir} && find -name '*.gcno' \
| xargs --replace=xx dirname xx \
| uniq \
| sed -e 's:^\./::')
#echo "primarydirs=${primarydirs}"
cmd="${lcov} --output ${outputstem}.info --capture --ignore-errors source"
for bdir in ${primarydirs}; do
sdir=$(dirname $(dirname ${bdir}))
cmd="${cmd} --base-directory ${srcdir}/${sdir} --directory ${builddir}/${bdir}"
done
#echo cmd=${cmd}
set -x
# capture
${cmd}
# keep only files with paths under source tree
# (don't want coverage for external libraries such as libstdc++ etc)
${lcov} --extract ${outputstem}.info "${srcdir}/*" --output ${outputstem}2.info
# remove unit test dirs
# (we're interested in coverage of our installed code, not of the unit tests that exercise it)
${lcov} --remove ${outputstem}2.info '*/utest/*' --output ${outputstem}3.info
# generate .html tree
mkdir -p ${builddir}/ccov/html
${genhtml} --ignore-errors source --show-details --prefix ${srcdir} --output-directory ${builddir}/ccov/html ${outputstem}3.info
# also send report to stdout
${lcov} --list ${outputstem}3.info

View file

@ -15,7 +15,7 @@ if (("${CMAKE_MODULE_PATH}" STREQUAL "") OR ("${CMAKE_MODULE_PATH}" STREQUAL pre
endif()
if (NOT XO_SUBMODULE_BUILD)
message("-- GUESSED_CMAKE_CMD=cmake -DCMAKE_CXX_STANDARD=${CMAKE_CXX_STANDARD} -DCMAKE_MODULE_PATH=${CMAKE_MODULE_PATH} -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -B ${CMAKE_BINARY_DIR}")
message("-- GUESSED_CMAKE_CMD=cmake -DCMAKE_CXX_STANDARD=${CMAKE_CXX_STANDARD} -DCMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX} -DCMAKE_MODULE_PATH=${CMAKE_MODULE_PATH} -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -B ${CMAKE_BINARY_DIR}")
message("-- CMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX}")
message("-- CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}")
endif()
@ -23,4 +23,5 @@ endif()
# needs to have been installed somewhere on CMAKE_MODULE_PATH,
# (e.g. from xo-cmake with the same value for CMAKE_INSTALL_PREFIX)
#
include(xo_macros/xo-project-macros)
#include(xo_macros/xo-project-macros)
include(xo_macros/xo_cxx) # not using v1 code-coverage; testing cmake-examples impl instead

110
docs/CMakeLists.txt Normal file
View file

@ -0,0 +1,110 @@
# xo-stringliteral/docs/CMakeLists.txt
if (XO_SUBMODULE_BUILD)
# in submodule build, rely on toplevel docs/CMakeLists.txt file instead
else()
# build docs starting from here only in standalone build.
# otherwise use top-level doxygen setup instead.
set(ALL_LIBRARY_TARGETS xo_stringliteral) # todo: automate this from xo-cmake macros
set(ALL_UTEST_TARGETS xo_stringliteral_ex1 ) # todo: automate this from xo-cmake macros
# look for doxygen executable
find_program(DOXYGEN_EXECUTABLE NAMES doxygen REQUIRED)
message("-- DOXYGEN_EXECUTABLE=${DOXYGEN_EXECUTABLE}")
# look for sphinx-build executable
find_program(SPHINX_EXECUTABLE NAMES sphinx-build REQUIRED)
message("-- SPHINX_EXECUTABLE=${SPHINX_EXECUTABLE}")
set(DOX_CONFIG_FILE ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
set(DOX_INPUT_DIR ${PROJECT_SOURCE_DIR})
set(DOX_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/dox)
set(DOX_INDEX_FILE ${DOX_OUTPUT_DIR}/html/index.html)
# .hpp files reachable from xo-stringliteral/include
#
# REMINDER: for reliability will need to re-run cmake when the set of .hpp files changes
#
file(GLOB_RECURSE DOX_HPP_FILES_GLOB ${PROJECT_SOURCE_DIR}/include *.hpp)
set(SPHINX_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/sphinx/html)
set(SPHINX_INDEX_FILE ${SPHINX_OUTPUT_DIR}/index.html)
#
# sphinx .rst files reachable from cmake-examples/docs
#
# REMINDER: for reliability will need to re-run cmake when the set of .rst files changes
#
file(GLOB_RECURSE SPHINX_RST_FILES_GLOB ${CMAKE_CURRENT_SOURCE_DIR} *.rst)
set(SPHINX_RST_FILES index.rst install.rst lessons.rst flatstring-reference.rst flatstring-class.rst)
# TODO:
# 1. move Doxyfile.in to xo-cmake project
# 2. replace this command section with xo-cmake macro
#
configure_file(
Doxyfile.in ${DOX_CONFIG_FILE}
FILE_PERMISSIONS OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE
@ONLY)
set(DOX_DEPS ${ALL_LIBRARY_TARGETS} ${ALL_UTEST_TARGETS} ${DOX_HPP_FILES_GLOB})
file(MAKE_DIRECTORY ${DOX_OUTPUT_DIR})
add_custom_command(
OUTPUT ${DOX_INDEX_FILE}
DEPENDS ${DOX_DEPS}
COMMAND "${DOXYGEN_EXECUTABLE}" ${DOX_CONFIG_FILE}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
MAIN_DEPENDENCY ${DOX_CONFIG_FILE}
COMMENT "Generating docs (doxygen)")
# To build this target
# $ cmake --build .build -j -- doxygen
# or
# $ cd .build
# $ make doxygen
#
add_custom_target(
doxygen
DEPENDS ${DOX_INDEX_FILE} ${DOX_DEPS}
)
# root of sphinx doc tree
set(SPHINX_SOURCE ${CMAKE_CURRENT_SOURCE_DIR})
set(SPHINX_DEPS doxygen conf.py ${SPHINX_RST_FILES} ${SPHINX_RST_FILES_GLOB} ${DOX_DEPS})
add_custom_command(
OUTPUT ${SPHINX_INDEX_FILE}
DEPENDS ${SPHINX_DEPS}
COMMAND ${SPHINX_EXECUTABLE}
-b html -Dbreathe_projects.xodoxxml=${CMAKE_CURRENT_BINARY_DIR}/dox/xml
${SPHINX_SOURCE} ${SPHINX_OUTPUT_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Generating docs (sphinx) -> [${SPHINX_OUTPUT_DIR}]")
# make sphinx --> generate sphinx documentation
#
add_custom_target(
sphinx
DEPENDS ${SPHINX_INDEX_FILE})
# - html docs generated in build/docs/sphinx
# - copy the doc tree to share/doc/xo_unit/html
#
# OPTIONAL: install directory tree if it exists,
# but don't complain if it's missing
install(
DIRECTORY ${SPHINX_OUTPUT_DIR}
FILE_PERMISSIONS OWNER_READ GROUP_READ WORLD_READ
DESTINATION ${CMAKE_INSTALL_PREFIX}/share/doc/${PROJECT_NAME}
COMPONENT Documentation
OPTIONAL)
# make docs --> generate sphinx documentation
add_custom_target(
docs
DEPENDS sphinx)
endif()

2816
docs/Doxyfile.in Normal file

File diff suppressed because it is too large Load diff

1
docs/_static/README vendored Normal file
View file

@ -0,0 +1 @@
add any static {.html, .js, ..} files for sphinx to pickup here

35
docs/conf.py Normal file
View file

@ -0,0 +1,35 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'xo stringlit documentation'
copyright = '2024, Roland Conybeare'
author = 'Roland Conybeare'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
#extensions = []
extensions = [ "breathe",
"sphinx.ext.autodoc" # generate info from docstrings
]
# note: breathe requires doxygen xml output -> must have GENERATE_XML = YES in Doxyfile.in
# match project name in Doxyfile.in
breathe_default_project = "xodoxxml"
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
pygments_style = 'sphinx'
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
#html_theme = 'alabaster'
html_theme = 'sphinx_rtd_theme'
html_static_path = ['_static']

60
docs/flatstring-class.rst Normal file
View file

@ -0,0 +1,60 @@
.. _flatstring-class:
Flatstring
==========
.. code-block:: cpp
#include <xo/flatstring/flatstring.hpp>
A ``flatstring`` is a thin wrapper around a character array.
.. doxygenclass:: xo::flatstring
Instance Variables
------------------
.. doxygengroup:: flatstring-instance-variables
:content-only:
Constants
---------
.. doxygengroup:: flatstring-constants
:content-only:
Constructors
------------
.. doxygengroup:: flatstring-ctor
:content-only:
Properties
----------
.. doxygengroup:: flatstring-properties
:content-only:
Access Methods
--------------
.. doxygengroup:: flatstring-access
:content-only:
Iterators
---------
.. doxygengroup:: flatstring-iterators
:content-only:
Assignment
----------
.. doxygengroup:: flatstring-assign
:content-only:
Conversion
----------
.. doxygengroup:: flatstring-conversion-operators
:content-only:

View file

@ -0,0 +1,15 @@
.. _flatstring_functions:
.. toctree::
:maxdepth: 2
Flatstring Functions
====================
.. code-block:: cpp
#include <xo/flatstring/flatstring.hpp>
.. doxygenfunction:: xo::flatstring_concat
.. doxygenfunction:: xo::flatstring_compare

View file

@ -0,0 +1,11 @@
.. _flatstring-reference:
Flatstring Reference
====================
.. toctree::
:maxdepth: 2
:caption: Flatstring Reference
flatstring-class
flatstring-functions

38
docs/index.rst Normal file
View file

@ -0,0 +1,38 @@
xo-flatstring documentation master file
xo-flatstring documentation
===========================
xo-flatstring is a lightweight header-only library that provides a constexpr
fixed-size no-allocation string implementation.
Why ``flatstring``?
1. ``flatstring`` instances can be used as template arguments. [1]_
2. ``flatstring`` operations (construction, concatenation, ...) are ``constexpr``, so can be done at compile time. [2]_
3. a ``flatstring`` expression can occupy both compile-time and runtime roles. [3]_
.. [1] A fixed-size char array *can* be used as a template
argument, but char* pointers cannot. Automatic conversion of char arrays to pointers in various contexts
makes them difficult to work with in c++ templates.
.. [2] Although allocation is permitted in constexpr code, it's subject to several restrictions.
it's not yet possible (as of c++23) to use ``std::string`` at compile time.
.. [3] contrast with a solution relying on template arguments, which must then be compile-time-only.
.. toctree::
:maxdepth: 2
:caption: xo-flatstring contents:
install
lessons
flatstring-reference
Indices and Tables
------------------
* :ref:`genindex`
* :ref:`search`

56
docs/install.rst Normal file
View file

@ -0,0 +1,56 @@
.. _install:
.. toctree
:maxdepth: 2
Install
=======
`xo-flatstring source`_ lives on github.
.. _xo-flatstring source: https://github.com/rconybea/xo-flatstring
Implementation relies on c++20 features (for example class-instances as template arguments).
Tested with gcc 13.2
Include as submodule
--------------------
.. code-block:: bash
cd myproject
git submodule add -b main https://github.com/rconybea/xo-flatstring ext/xo-flatstring
git submodule update --init
This assumes you organize directly-incorporated dependencies under directory ``myproject/ext``.
You would then add ``myproject/ext/xo-flatstring/include`` to your compiler's include path,
and from c++ do something like
.. code-block:: c++
#include <xo/flatstring/flatstring.hpp>
in c++ source files that rely on xo-flatstring
Supported compilers
-------------------
* developed with gcc 13.2.0; github CI using gcc 11.4.0 (asof April 2024)
Building from source
--------------------
Although the xo-flatstring library is header-only, unit tests have some dependencies.
Example instructions (github CI) for build starting from stock ubuntu are in `ubuntu-main.yml`_
.. _ubuntu-main.yml: https://github.com/Rconybea/xo-flatstring/blob/main/.github/workflows/ubuntu-main.yml
Unit test dependencies:
* `catch2`_ header-only unit-test framework
* `xo-cmake`_ cmake macros
* `xo-indentlog`_ logging with call-structure indenting
.. _catch2: https://github.com/catchorg/Catch2
.. _xo-cmake: https://github.com/rconybea/xo-cmake
.. _xo-indentlog: https://github.com/rconybea/indentlog

44
docs/lessons.rst Normal file
View file

@ -0,0 +1,44 @@
.. _lessons:
.. toctree
:maxdepth: 2
Lessons
=======
This is a rogue's gallery of experiments, typically unsuccessful.
One hurdle we've created for ourselves, is we need both gcc and clang to agree
that an expression can be computed at compile-time;
otherwise will get false alarms in our IDE (raised by LSP running in the background, which relies on clang).
Must Fully Initialize Memory
----------------------------
Struggled for a while with the implementation of :ref:xo::flatstring_concat
.. code-block:: cpp
template <std::size_t N>
flatstring::flatstring<N>() {
if (N > 0)
value_[0] = '\0';
}
This implementation satisfies gcc, but not clang: in the following snippet, clang doesn't recognize ``tmp`` as constexpr:
.. code-block:: cpp
constexpr n = ...;
flatstring<n> tmp;
static_assert(tmp.size() == ...); // tmp not constexpr!
Correction is to prove to clang that every memory address owned by an empty ``flatstring`` is initialized:
.. code-block:: cpp
template <std::size_t N>
flatstring::flatstring<N>() {
std::fill_n(value_, N, '\0');
}

View file

@ -1,38 +1,165 @@
/* @file ex1.cpp */
#include "xo/stringliteral/stringliteral.hpp"
#include "xo/stringliteral/stringliteral_iostream.hpp"
#include "xo/stringliteral/string_view_concat.hpp"
#include "xo/flatstring/flatstring.hpp"
//#include "xo/stringliteral/stringliteral_iostream.hpp"
#include "xo/flatstring/experiment.hpp"
//#include "xo/stringliteral/string_view_concat.hpp"
#include <iostream>
int
main() {
using namespace std;
using xo::stringliteral;
using xo::flatstring;
#ifdef WAITAMO
using xo::stringliteral_compare;
#endif
static_assert(foo1().x_ == 1);
static_assert(foo1().y_ == 2);
constexpr foo1 s1;
static_assert(s1.x_ == 1);
static_assert(s1.y_ == 2);
constexpr foo2 s2;
static_assert(s2.v_[0] == 'a');
static_assert(s2.v_[1] == 'b');
constexpr foo3<2> s3;
static_assert(s3.v_[0] == 'a');
static_assert(s3.v_[1] == 'b');
constexpr foo4<6> s4("hello");
constexpr foo5 s5("hello");
static_assert(s5.v_[0] == 'h');
static_assert(s5.v_[5] == '\0');
constexpr foo6 s6("hello", ", world!");
static_assert(s6.v_[0] == 'h');
static_assert(s6.size() == 13);
cerr << "s6=" << s6.c_str() << endl;
/* z gives allocation size. string size is z-1 */
constexpr std::size_t z = concat_size("hello", ", world!", " What's up?");
static_assert(z == 25);
constexpr foo7<10> s7("Hello", ", world!", " What's up?");
constexpr stringlit<10> s8("0123", "45678");
static_assert(s8.size() == 9);
constexpr std::size_t z8 = stringlit_capacity(s8);
static_assert(sizeof("0123") == 5);
static_assert(sizeof("45") == 3);
static_assert(sizeof("78") == 3);
static_assert(literal_strlen("0123") == 4);
static_assert(z8 == 10);
#ifdef NOT_USING
constexpr stringliteral s1("hello");
static_assert(count_size("0123") == 5);
static_assert(count_size("0123", "45") == 7);
static_assert(count_size("0123", "45", "67", "8") == 10);
constexpr auto z9 = count_size("0123", "45", "78");
static_assert(z9 == 9);
constexpr auto z10 = foofn("0123");
static_assert(z10 == 5);
#endif
//constexpr auto z11 = foofn2("0123");
//static_assert(z9 > 22);
constexpr auto s9 = stringlit_make("0123", "456", "78");
//constexpr auto s9 = stringlit_makepalooza("0123", "45678");
static_assert(s9.size() == 9);
constexpr auto s10 = stringlit_make("0", "123", "456", "78");
static_assert(s10.size() == 9);
cerr << s10.c_str() << endl;
#ifdef NOT_SUCCESSFUL
constexpr auto s11 = stringlit_make("0", "1", "23", "456", "78");
#endif
constexpr std::size_t z9 = stringlit_capacity(s9, s10);
static_assert(z9 == 19);
constexpr auto s12 = stringlit_cat(s9, s10);
static_assert(s12.size() == 18);
cerr << s12.c_str() << endl;
constexpr auto s13 = stringlit_cat(s9, s10, s12);
static_assert(s13.size() == 36);
cerr << s13.c_str() << endl;
#ifdef NOT_USING
static_assert(stringliteral_compare(s1, s1) == 0);
cerr << s1 << endl;
constexpr stringliteral s2 = stringliteral_concat(stringliteral("hello"),
stringliteral(", world"));
#endif
constexpr flatstring s14 = flatstring_concat(flatstring("foo"), flatstring("bar"));
static_assert(s14.fixed_capacity == 7);
static_assert(sizeof(s14) == 7);
constexpr flatstring s15 = flatstring_concat(flatstring("hello"),
", ",
flatstring("world"));
static_assert(s15.fixed_capacity == 13);
static_assert(sizeof(s15) == 13);
constexpr auto s16 = xo::flatstring_concat("foo", "bar");
static_assert(s16.fixed_capacity == 7);
constexpr auto cmp = flatstring_compare(s14, s14);
static_assert(cmp == 0);
#ifdef WAITAMO
constexpr stringliteral s2 = stringliteral_stringlit_make(stringliteral("hello"),
stringliteral(", world"));
#endif
#ifdef NOT_USING
static constexpr string_view hello("hello");
static constexpr string_view world(" world");
static constexpr auto s2 = concat_v<hello, world>;
static constexpr auto s3 = stringlit_make_v<hello, world>;
static constexpr string_view hello_world("hello world");
static_assert(s2 == hello_world);
static_assert(s3 == hello_world);
cerr << hello_world << endl;
#endif
}
/* end ex1.cpp */

View file

@ -0,0 +1,414 @@
/** @file flatstring.hpp
*
* Author: Roland Conybeare
**/
#pragma once
#include <string_view>
#include <sstream>
#include <algorithm>
#include <memory>
namespace xo {
/** @class flatstring
* @brief class to represent a string with a fixed amount of storage space.
*
* - Flatstring memory layout is a fixed-size, null-terminated char array.
* - With a few exceptions, flatstring methods are noexcept.
* @c flatstring<N>::at() may throw, for consistency with @c std::string::at() behavior
* - Construction and concatenation of flatstrings are constexpr,
* and can be done at compile time.
* We rely on this in related projects (e.g. https://github.com:rconybea/xo-unit)
* - Preserves as much of the c++23 @c std::string api as practicable
*
* @c N includes mandatory null terminator, so we require @c N > 0.
*
* @invariant all flatstring instances are null-terminated.
* @invariant sizeof(flatstring<N>) == N
**/
template <std::size_t N>
struct flatstring {
/** @defgroup flatstring-types template types **/
///@{
using traits_type = std::char_traits<char>;
using value_type = char;
using allocator_type = std::allocator<char>;
using size_type = std::allocator_traits<allocator_type>::size_type;
using difference_type = std::allocator_traits<allocator_type>::difference_type;
using reference = value_type &;
using const_reference = const value_type &;
using pointer = std::allocator_traits<allocator_type>::pointer;
using const_pointer = std::allocator_traits<allocator_type>::const_pointer;
using iterator = char *;
using const_iterator = const char *;
using reverse_iterator = char *;
using const_reverse_iterator = const char *;
///@}
/** @defgroup flatstring-constants constants **/
///@{
static constexpr const size_type npos = size_type(-1);
/** @brief capacity of this flatstring, including final null terminator.
*
* @note not present in @c std::string api
**/
static constexpr const std::size_t fixed_capacity = N;
///@}
public:
/** @defgroup flatstring-ctor constructors **/
///@{
/** @brief create empty string literal. Will contain N null characters
*
* Example
* @code
* constexpr flatstring<5> s1;
* static_assert(s1.empty());
* @endcode
**/
constexpr flatstring() noexcept {
/* note: clang verifies that we fully initialize memory; otherwise will not recognize
* instance as constexpr
*/
std::fill_n(value_, N, '\0');
}
/** @brief create string literal from a correctly-sized char array
*
* Example
* @code
* constexpr flatstring s1("hello");
* static_assert(s1.size() > 0);
* @endcode
**/
constexpr flatstring(const char (&str)[N]) noexcept {
std::copy_n(str, N, value_);
}
///@}
/** @defgroup flatstring-properties property-methods **/
///@{
/** @brief true if (and only if) string is empty **/
constexpr bool empty() const noexcept { return value_[0] == '\0'; }
/** @brief returns current size of this string **/
constexpr size_type size() const noexcept {
return this->cend() - this->cbegin();
}
/** @brief synonym for @c size() **/
constexpr size_type length() const noexcept { return size(); }
constexpr size_type capacity() const noexcept { return fixed_capacity - 1; }
constexpr size_type max_size() const noexcept { return fixed_capacity - 1; }
/** @brief contents as plain old C-style string. **/
constexpr const char * c_str() const noexcept { return value_; }
///@}
/** @defgroup flatstring-access access methods **/
///@{
/** @brief return char at position @p pos in this string (counting from zero).
*
* Throws @c std::out_of_range exception if @p pos >= @c N
**/
constexpr value_type & at(size_type pos) throw() { return this->at_aux(pos); }
constexpr const value_type & at(size_type pos) const throw() { return const_cast<flatstring *>(this)->at_aux(pos); }
/** @brief return char at position @p pos in this string (counting from zero).
*
* Does not check bounds: undefined behavior if @p pos >= @c N
*
* @pre 0<=pos<=N-1
**/
constexpr value_type & operator[](size_type pos) { return value_[pos]; }
constexpr const value_type & operator[](size_type pos) const { return value_[pos]; }
///@}
/** @defgroup flatstring-iterators iterators **/
///@{
constexpr iterator begin() { return &value_[0]; }
constexpr iterator end() { return this->last(); }
constexpr const_iterator cbegin() const { return &value_[0]; }
constexpr const_iterator cend() const { return const_cast<flatstring*>(this)->last<iterator>(); }
constexpr const_iterator begin() const { return cbegin(); }
constexpr const_iterator end() const { return cend(); }
constexpr reverse_iterator rbegin() { return this->last(); }
constexpr reverse_iterator rend() { return &value_[0]; }
constexpr const_reverse_iterator crbegin() const { return const_cast<flatstring*>(this)->last<iterator>(); }
constexpr const_reverse_iterator crend() const { return &value_[0]; }
constexpr const_reverse_iterator rbegin() const { return crbegin(); }
constexpr const_reverse_iterator rend() const { return crend(); }
///@}
/** @defgroup flatstring-assign assignment **/
///@{
/** @brief put string into empty state. fills entire char array with nulls **/
void clear() { std::fill_n(value_, N, '\0'); }
/** @brief replace contents with min(count,N-1) copies of character ch **/
constexpr flatstring & assign(size_type count, value_type ch) {
std::size_t pos = 0;
for (; pos < std::min(count, N-1); ++pos)
value_[pos] = ch;
for (; pos < N; ++pos)
value_[pos] = '\0';
return *this;
}
/** @brief replace contents with first N-1 characters of str **/
constexpr flatstring & assign(const flatstring & x) {
for (std::size_t pos = 0; pos < N-1; ++pos)
value_[pos] = x.value_[pos];
value_[N-1] = '\0';
return *this;
}
/** @brief replace contents with substring [pos,pos+count] of str **/
constexpr flatstring & assign(const flatstring & x,
size_type pos, size_type count = npos) {
std::size_t i = 0;
for (;
i < std::min(std::min(count,
std::max(x.capacity-1 - pos,
0)),
N-1);
++i)
value_[i] = x.value_[pos+i];
for (; i < N; ++i)
value_[i] = '\0';
return *this;
}
/** @brief replace contents with range [cstr, cstr + count) **/
constexpr flatstring & assign(const value_type * cstr, size_type count) {
std::size_t i = 0;
for (; i < std::min(N-1, count); ++i)
value_[i] = cstr[i];
for (; i < N; ++i)
value_[i] = '\0';
return *this;
}
/** @brief replace contents with C-style string cstr **/
constexpr flatstring & assign(const value_type * cstr) {
std::size_t i = 0;
const value_type * p = cstr;
while ((i < N-1) && (*p != '\0')) {
value_[i] = *p;
++i;
++p;
}
for (; i < N; ++i)
value_[i] = '\0';
return *this;
}
/** @brief replace contents with iterator range [first, last) **/
template <typename InputIter>
constexpr flatstring & assign(InputIter first, InputIter last) {
InputIter ix = first;
std::size_t i = 0;
for (; (i < N-1) && (ix != last); ++i) {
value_[i] = *ix;
}
for (; i < N; ++i)
value_[i] = '\0';
return *this;
}
///@}
// insert
// insert_range
// erase
// push_back
// append
// append_range
// operator+=
// replace
// replace_with_range
// copy
// find
// rfind
// find_first_of
// find_first_not_of
// find_last_of
// find_last_not_of
// compare
// starts_with
// end_with
// contains
// substr
/** @defgroup flatstring-conversion-operators conversion operators **/
///@{
/** @brief conversion to @c std::string
*
* Example
* @code
* constexpr flatstring s("bazinga!");
* std::string s_str{s.str()};
* @endcode
**/
std::string str() const { return std::string(value_); }
/** @brief conversion operator to string_view **/
constexpr operator std::string_view() const noexcept { return std::string_view(value_); }
/** @brief conversion operator to C-style string.
*
* Example
* @code
* constexpr flatstring s("obey gravity..");
* strcmp(s, "obey...");
* @endcode
**/
constexpr operator const char * () const { return value_; }
///@}
private:
constexpr value_type & at_aux(size_type pos) {
if (pos >= N) {
#ifdef NOT_USING
/* note: can't build stringstream at compile time */
std::stringstream ss;
ss << "flatstring<" << N << ">::at: expected pos=[" << pos << "] in interval [0," << N << ")" << std::endl;
#endif
throw std::out_of_range("at_aux: range error");
}
return (*this)[pos];
}
template <typename Iterator>
constexpr Iterator last() {
Iterator p = &value_[N-1];
/* search backward for first padding '\0' */
while ((p > &value_[0]) && (*(p-1) == '\0'))
--p;
return p;
}
public:
/** @defgroup flatstring-instance-variables instance variables **/
///@{
/** @brief characters comprising this literal string **/
char value_[N];
///@}
};
/** @brief sentinel type, for forbidden stringliteral with no space for a null terminator **/
template <>
struct flatstring<0> { flatstring() = delete; };
// non-member functions
// erase
// erase_if
// operator<<
// operator>>
// getline
// stoi
// stol
// stoll
// stoul
// stoull
// stof
// stod
// stold
#ifdef NOT_USING
/** @brief all_same_v<T1, .., Tn> is true iff types T1 = .. = Tn
**/
template < typename First, typename... Rest >
constexpr auto
all_same_v = std::conjunction_v< std::is_same<First, Rest>... >;
#endif
/** @brief Concatenate native or wrapped string literals
*
* Can mix stringliteral objects and native C-style string literals,
* and still preserve constexpr-ness.
*
* Example:
* @code
* constexpr auto s = stringliteral_concat(stringliteral("hello"),
* ", ",
* stringliteral("world"));
* static_assert(s.capacity == 13);
* @endcode
*
**/
template < typename... Ts>
constexpr auto
flatstring_concat(Ts && ... args) noexcept
{
#ifdef NOT_USING
static_assert(all_same_v<std::decay_t<Ts>...>,
"string must share the same char type");
using char_type = std::remove_const_t< std::remove_pointer_t < std::common_type_t < Ts... > > >;
#endif
using value_type = char;
/** n1: total number of bytes used by arguments **/
constexpr std::size_t n1 = (sizeof(Ts) + ...);
/** z1: each string arg has a null terminator included in its size,
* z1 is the number of arguments in parameter pack Ts,
* which equals the number of null terminators used
**/
constexpr std::size_t z1 = sizeof...(Ts);
/** n: number of chars in concatenated string. +1 for final null **/
constexpr std::size_t n
= (n1 / sizeof(value_type)) - z1 + 1;
flatstring<n> result;
std::size_t pos = 0;
auto detail_concat = [ &pos, &result ](auto && arg) {
constexpr auto count = (sizeof(arg) - sizeof(value_type)) / sizeof(value_type);
std::copy_n(/*arg.c_str()*/ static_cast<const char *>(arg), count, result.value_ + pos);
pos += count;
};
(detail_concat(args), ...);
return result;
}
/** @brief compare two string literals lexicographically.
*
* Example:
* @code
* constexpr auto cmp = stringliteral_compare(stringliteral("foo"), stringliteral("bar"));
* static_assert(cmp > 0);
* @endcode
**/
template <std::size_t N1,
std::size_t N2>
constexpr auto
flatstring_compare(const flatstring<N1> & s1,
const flatstring<N2> & s2) noexcept
{
return (std::string_view(s1.value_) <=> std::string_view(s2.value_));
}
/** @brief 3-way compare for two stringliterals **/
template <std::size_t N1,
std::size_t N2>
constexpr auto
operator<=>(const flatstring<N1> & s1,
const flatstring<N2> & s2) noexcept
{
return flatstring_compare(s1, s2);
}
} /*namespace xo*/
/** end stringliteral.hpp **/

View file

@ -0,0 +1,37 @@
/** @file flatstring_iostream.hpp
*
* Author: Roland Conybeare
**/
#pragma once
#include "flatstring.hpp"
#include <ostream>
//#include <cstdint>
namespace xo {
/** @brief print flatstring on stream os.
*
**/
template <std::size_t N>
void
print_flatstring (std::ostream & os, const flatstring<N> & x) {
os << x.c_str();
}
/** @brief print flatstring x on stream os.
*
* Example
* @code
* cout << flatstring("foo"); // outputs "foo"
* @endcode
**/
template <std::size_t N>
inline std::ostream &
operator<< (std::ostream & os, const flatstring<N> & x) {
print_flatstring(os, x);
return os;
}
} /*namespace xo*/
/** end flatstring_iostream.hpp **/

View file

@ -1,96 +0,0 @@
/** @file stringliteral.hpp
*
* Author: Roland Conybeare
**/
#pragma once
#include <string_view>
#include <algorithm>
#include <cstdint>
namespace xo {
/** @class stringliteral
*
* @brief class to represent a literal string at compile time, for use as template argument
**/
template <std::size_t N>
struct stringliteral {
constexpr stringliteral() { if (N > 0) value_[0] = '\0'; }
constexpr stringliteral(const char (&str)[N]) { std::copy_n(str, N, value_); }
constexpr int size() const { return N; }
constexpr char const * c_str() const { return value_; }
char value_[N];
};
/** @brief all_same_v<T1, .., Tn> is true iff types T1 = .. = Tn
**/
template < typename First, typename... Rest >
constexpr auto
all_same_v = std::conjunction_v< std::is_same<First, Rest>... >;
/** @brief concatenate string literals
*
* NOTE: this isn't constexpr in clang16
**/
template < typename... Ts>
constexpr auto
stringliteral_concat(Ts && ... args)
{
#ifdef NOT_USING
static_assert(all_same_v<std::decay_t<Ts>...>,
"string must share the same char type");
using char_type = std::remove_const_t< std::remove_pointer_t < std::common_type_t < Ts... > > >;
#endif
using char_type = char;
/** n1: total number of bytes used by arguments **/
constexpr size_t n1 = (sizeof(Ts) + ...);
/** z1: each string arg has a null terminator included in its size,
* z1 is the number of arguments in parameter pack Ts,
* which equals the number of null terminators used
**/
constexpr size_t z1 = sizeof...(Ts);
/** n: number of chars in concatenated string. +1 for final null **/
constexpr size_t n
= (n1 / sizeof(char_type)) - z1 + 1;
stringliteral<n> result;
size_t pos = 0;
auto detail_concat = [ &pos, &result ](auto && arg) {
constexpr auto count = (sizeof(arg) - sizeof(char_type)) / sizeof(char_type);
std::copy_n(arg.c_str(), count, result.value_ + pos);
pos += count;
};
(detail_concat(args), ...);
//return stringliteral("");
return result;
}
#ifdef NOT_USING
template <std::size_t N1, std::size_t N2>
constexpr auto
stringliteral_compare(stringliteral<N1> && s1, stringliteral<N2> && s2)
{
return std::string_view(s1.value_) <=> std::string_view(s2.value_);
}
#endif
template <std::size_t N1, std::size_t N2>
constexpr auto
stringliteral_compare(const stringliteral<N1> & s1, const stringliteral<N2> & s2)
{
return std::string_view(s1.value_) <=> std::string_view(s2.value_);
}
} /*namespace xo*/
/** end stringliteral.hpp **/

View file

@ -1,36 +0,0 @@
/** @file stringliteral_iostream.hpp
*
* Author: Roland Conybeare
**/
#pragma once
#include "stringliteral.hpp"
#include <ostream>
//#include <cstdint>
namespace xo {
/** @brief print stringliteral on stream os.
*
**/
template <std::size_t N>
void
print_stringliteral (std::ostream & os, const stringliteral<N> & x) {
os << x.c_str();
}
/** @brief print stringliteral x on stream os.
*
* Example
* @code
* cout << stringliteral("foo"); // outputs "foo"
**/
template <std::size_t N>
inline std::ostream &
operator<< (std::ostream & os, const stringliteral<N> & x) {
print_stringliteral(os, x);
return os;
}
} /*namespace xo*/
/** end stringliteral_iostream.hpp **/

View file

@ -0,0 +1 @@
roland@roly-desktop-23.717424:1712774400

21
utest/CMakeLists.txt Normal file
View file

@ -0,0 +1,21 @@
# xo-flatstring/utest/CMakeLists.txt
set(SELF_EXE utest.flatstring)
set(SELF_SRCS
flatstring_utest_main.cpp
flatstring.test.cpp)
add_executable(${SELF_EXE} ${SELF_SRCS})
xo_include_options2(${SELF_EXE})
add_test(NAME ${SELF_EXE} COMMAND ${SELF_EXE})
#target_code_coverage(${SELF_EXE} AUTO ALL)
# ----------------------------------------------------------------
# deps: logutils, ...
xo_self_headeronly_dependency(${SELF_EXE} xo_flatstring)
xo_dependency(${SELF_EXE} indentlog)
xo_external_target_dependency(${SELF_EXE} Catch2 Catch2::Catch2)
# end CMakeLists.txt

131
utest/flatstring.test.cpp Normal file
View file

@ -0,0 +1,131 @@
/** @file flatstring.utest.cpp **/
#include "xo/stringliteral/stringliteral.hpp"
#include "xo/indentlog/scope.hpp"
#include "xo/indentlog/print/tag.hpp"
#include <catch2/catch.hpp>
//#include <iostream>
namespace xo {
using namespace std;
namespace ut {
template <typename String>
void
flatstring_runtime_tests(const String & str, const char * text) {
INFO(tostr(XTAG(str), XTAG(text)));
REQUIRE(str.fixed_capacity == strlen(text)+1);
REQUIRE(str.capacity() == strlen(text));
REQUIRE(str.size() == strlen(text));
REQUIRE(str.length() == strlen(text));
REQUIRE(strcmp(str.c_str(), text) == 0);
REQUIRE(strcmp(str, text) == 0);
/* verify range iteration visits contents in order */
{
size_t i = 0;
for (char ch : str) {
INFO(XTAG(i));
CHECK(ch == text[i]);
++i;
}
}
}
/* using macro here because template argument depends on size of literal C string,
* and we can't use such a string as a template argument.
*
* static_asserts: using these to verify that constexpr methods are being computed
* at compile time.
*
* REQUIRE() calls to do verification that relies on non-constexpr calls such as
* strlen(), strcmp()
*/
# define LITERAL_TEST_BODY(name, text) \
constexpr flatstring name{text}; \
static_assert(name[0]==text[0]); \
static_assert(name.at(0)==text[0]); \
static_assert(name.empty() == true || name.empty() == false); \
static_assert(name.capacity() >= 0); \
static_assert(name.begin() != nullptr); \
static_assert(name.end() != nullptr); \
static_assert(name.cbegin() != nullptr); \
static_assert(name.cend() != nullptr); \
static_assert(name.rbegin() != nullptr); \
static_assert(name.rend() != nullptr); \
static_assert(name.crbegin() != nullptr); \
static_assert(name.crend() != nullptr); \
static_assert(name.size() >= 0); \
static_assert(name.c_str() != nullptr); \
static_assert((name <=> name) == 0); \
static_assert(name == name); \
static_assert(name >= name); \
static_assert(name <= name); \
static_assert(!(name != name)); \
static_assert(!(name > name)); \
static_assert(!(name < name)); \
flatstring_runtime_tests(name, text); \
REQUIRE(name.fixed_capacity == strlen(text)+1); \
REQUIRE(name.capacity() == strlen(text)); \
REQUIRE(name.size() == strlen(text)); \
REQUIRE(name.length() == strlen(text)); \
REQUIRE(strcmp(name.c_str(), text) == 0); \
REQUIRE(strcmp(name, text) == 0); \
static_assert(string_view(name) == string_view(name)); \
TEST_CASE("flatstring", "[flatstring][compile-time]") {
constexpr bool c_debug_flag = false;
// can get bits from /dev/random by uncommenting the 2nd line below
//uint64_t seed = xxx;
//rng::Seed<xoshio256ss> seed;
//auto rng = xo::rng::xoshiro256ss(seed);
scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.flatstring"));
//log && log("(A)", xtag("foo", foo));
/* mostly compile-time tests here */
LITERAL_TEST_BODY(s1, "h");
LITERAL_TEST_BODY(s2, "he");
LITERAL_TEST_BODY(s3, "hel");
LITERAL_TEST_BODY(s4, "hell");
LITERAL_TEST_BODY(s5, "hello");
LITERAL_TEST_BODY(s6, "hello,");
LITERAL_TEST_BODY(s7, "hello, ");
LITERAL_TEST_BODY(s8, "hello, w");
LITERAL_TEST_BODY(s9, "hello, wo");
LITERAL_TEST_BODY(s10, "hello, wor");
LITERAL_TEST_BODY(s11, "hello, worl");
LITERAL_TEST_BODY(s12, "hello, world");
LITERAL_TEST_BODY(s13, "hello, world!");
static_assert(s1 != s2);
static_assert(s2 != s3);
static_assert(s3 != s4);
static_assert(s4 != s5);
static_assert(s12 != s13);
static_assert(s1 < s2);
static_assert(s2 < s3);
static_assert(s3 < s4);
static_assert(s4 < s5);
static_assert(s12 < s13);
static_assert(s2 > s1);
static_assert(s3 > s2);
static_assert(s4 > s3);
static_assert(s5 > s4);
static_assert(s13 > s12);
} /*TEST_CASE(flatstring)*/
} /*namespace ut*/
} /*namespace xo*/
/** end flatstring.utest.cpp **/

View file

@ -0,0 +1,6 @@
/* @file flatstring_utest_main.cpp */
#define CATCH_CONFIG_MAIN
#include "catch2/catch.hpp"
/* end flatstring_utest_main.cpp */