Compare commits

..
Author SHA1 Message Date
Gennaro Prota 2ff9d3535e Don't qualify string_view in the available() test's pos/count append() case
Reason: Using the basic_string_view type used by the library.

This fixes the previous commit.
2026-04-24 07:05:02 +02:00
Gennaro Prota 8c44a96c8d Use std::string_view in the available() test's pos/count append() example
The test I added with available() passed "xyzwv" directly to the 3-arg
append(v, pos, count). That overload's viewable-T constraint excludes
const CharT*-convertible types (to avoid clashing with append(const
CharT*, size_type)), so the C-string literal doesn't match and there is
no viable 3-arg overload. Wrap the literal in std::string_view.
2026-04-24 06:41:11 +02:00
Gennaro Prota bf20b809ec Add basic_static_string::available()
This returns max_size() - size(), i.e. the number of characters that can
still be inserted before the string reaches its capacity. Intended as a
shorthand for capacity-capped writes in combination with the
pos/count-taking overloads of append, assign, and insert:

  str.append(sv, 0, str.available());

Addresses the ergonomic complaint in issue #81 without expanding the
overload set.

Closes issue #81.
2026-04-22 10:06:00 +02:00
Gennaro Prota 9271183ea8 Fix the basic_static_cstring array ctor to respect the no-embedded-NULs invariant
The constructor taking a const CharT (&)[M] copied M - 1 characters
verbatim via assign(arr, M - 1), preserving any embedded NULs in the
input. This would violate the type's documented "no embedded NULs"
invariant.

  const char arr[] = { '1', '2', '\0', '4', '5', '\0' };
  static_cstring<5> s(arr);
  // data_ holds {'1','2','\0','4','5','\0'} --- embedded NUL stored

Delegate to assign(arr) instead, which uses traits::length() to
determine the size. This matches the behavior of the other
CharT-accepting entry points (assign(const CharT*), the const CharT*
constructor) and makes the invariant self-enforcing at construction.

Note: In practice, this is a bug that would never surface, because the
array constructor is never chosen for static_cstring<N> s(arr)---the
pointer constructor always wins---but the array constructor is needed
for CTAD (deduction guide), so it better be correct.

This also means the test we added is, at the moment, superfluous.
2026-04-21 16:14:06 +02:00
Gennaro Prota 1c3143c18b Fix a static_assert message
The message assumed the argument to the constructor was a string
literal, but that's not required.
2026-04-21 16:14:06 +02:00
Gennaro Prota 62ed5ee17a Fix basic_static_cstring::compare(const CharT*) for over-long inputs
compare(const CharT* s) constructed a temporary basic_static_cstring<N>
from s and delegated to the class-type overload. The constructor throws
std::length_error when traits::length(s) > N, and because compare() is
noexcept, the throw led to std::terminate():

  static_cstring<3> s("abc");
  s.compare("abcd");              // terminate() via noexcept throw

The free operator==() / operator!=() overloads for const CharT*, which
delegate to compare(), had the same flaw.
2026-04-21 16:13:29 +02:00
Gennaro Prota c6cd8074ab Fix the comparison operators for basic_static_cstring
The defaulted operators had the wrong semantics, as they compared the
entire data_ buffer. The bug showed up when shrinking:

  static_cstring<10> s("hello");
  s.assign("hi", 2);              // data_[3..4] still "lo"
  static_cstring<10> fresh("hi");
  assert(s == fresh);             // FAILED
2026-04-21 11:59:41 +02:00
Gennaro Prota 54eda39d91 Fix a typo in a comment ("compiles" for "compilers") 2026-04-21 11:33:22 +02:00
Gennaro Prota e9313dc331 Add an experimental basic_static_cstring template in example/
This introduces an alternative to basic_static_string designed for use
in POD types: Trivially copyable, having a sizeof == N + 1, with no
embedded NULs.

Placed in example/ to gather user feedback before committing to a public
API. See issue #23.
2025-12-19 19:32:30 +01:00
Gennaro Prota 0c5e5b8e58 Fix our deduction guide 2025-12-19 18:26:40 +01:00
Gennaro Prota 6f0c00b268 Work around a Clang 3.7 bug affecting constexpr insert()
The iterator-based insert(const_iterator, size_type, value_type)
function relies on traits_type::move() to shift the existing null
terminator to its new position. Clang 3.7's constexpr evaluator does not
handle this correctly, causing the following test to fail:

  static_string<3>{"ab"}.insert(2, 1, 'c') == "abc"

Add an explicit term() call, guarded by a preprocessor conditional for
Clang 3.7, to ensure proper null termination.
2025-12-19 18:23:33 +01:00
Gennaro Prota 2175496c55 Condition the NTTP tests on __cpp_nontype_template_args
They were conditioned on detection of C++20 via __cplusplus, but Clang
10 and 11 don't support class types as NTTP, even though they report
C++20 via __cplusplus when -std=c++20 is used.
2025-12-19 18:23:33 +01:00
Gennaro Prota aee3c62957 Apply the workaround in the previous commit to Clang 3.7 and 9-19, too
Reason: They have the same issue as GCC 9.
2025-12-19 18:23:33 +01:00
Gennaro Prota 0e28b358dc Work around GCC 9 rejecting a legitimate pointer comparison in a constexpr context 2025-12-19 18:23:33 +01:00
Gennaro Prota 4dcfb39494 Replace the implementation of cxper_char_traits::move() with a simpler one
Reason: See the new code comment.
2025-12-19 18:23:33 +01:00
Gennaro Prota 4bf6461ca1 Work around a bug in GCC 5-10
GCC 5-10 incorrectly complain about our nested classes being private.
So, make them public.
2025-12-19 18:23:33 +01:00
Krystian Stasiowski 67efdf6a9b Make basic_static_string usable as a NTTP 2025-12-19 18:23:33 +01:00
Krystian Stasiowski 3a410b8472 Add build directory and CMake preset files to .gitignore 2025-12-19 18:23:33 +01:00
6 changed files with 329 additions and 63 deletions
+128 -10
View File
@@ -25,14 +25,132 @@ on:
- meta/**
- README.md
env:
B2_TARGETS: libs/$SELF/example
jobs:
call-boost-ci:
name: Run Boost.CI
uses: boostorg/boost-ci/.github/workflows/reusable.yml@master
with:
exclude_cxxstd: '98,03,0x,11,14,17'
enable_pr_coverage: false
enable_multiarch: false
linux:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- { toolset: gcc-13, cxxstd: '20,23' }
- { toolset: gcc-14, cxxstd: '20,23,26' }
- { toolset: clang-17, cxxstd: '20,23' }
- { toolset: clang-18, cxxstd: '20,23,26' }
steps:
- name: Checkout Boost super-project
uses: actions/checkout@v4
with:
repository: boostorg/boost
ref: develop
fetch-depth: 0
- name: Checkout this library
uses: actions/checkout@v4
with:
path: libs/static_string
fetch-depth: 0
- name: Initialize Boost submodules
run: |
git submodule update --init tools/boostdep
python tools/boostdep/depinst/depinst.py --git_args '--jobs 4' static_string
- name: Bootstrap b2
run: ./bootstrap.sh
- name: Generate Boost headers
run: ./b2 headers
- name: Build and run example tests
run: |
./b2 libs/static_string/example/static_cstring \
toolset=${{ matrix.toolset }} \
cxxstd=${{ matrix.cxxstd }} \
variant=debug,release \
-j$(nproc)
macos:
runs-on: macos-14
strategy:
fail-fast: false
matrix:
include:
- { toolset: clang, cxxstd: '20,23' }
steps:
- name: Checkout Boost super-project
uses: actions/checkout@v4
with:
repository: boostorg/boost
ref: develop
fetch-depth: 0
- name: Checkout this library
uses: actions/checkout@v4
with:
path: libs/static_string
fetch-depth: 0
- name: Initialize Boost submodules
run: |
git submodule update --init tools/boostdep
python3 tools/boostdep/depinst/depinst.py --git_args '--jobs 4' static_string
- name: Bootstrap b2
run: ./bootstrap.sh
- name: Generate Boost headers
run: ./b2 headers
- name: Build and run example tests
run: |
./b2 libs/static_string/example/static_cstring \
toolset=${{ matrix.toolset }} \
cxxstd=${{ matrix.cxxstd }} \
variant=debug,release \
-j$(sysctl -n hw.ncpu)
windows:
runs-on: windows-2022
strategy:
fail-fast: false
matrix:
include:
- { toolset: msvc-14.3, cxxstd: '20,latest' }
steps:
- name: Checkout Boost super-project
uses: actions/checkout@v4
with:
repository: boostorg/boost
ref: develop
fetch-depth: 0
- name: Checkout this library
uses: actions/checkout@v4
with:
path: libs/static_string
fetch-depth: 0
- name: Initialize Boost submodules
run: |
git submodule update --init tools/boostdep
python tools/boostdep/depinst/depinst.py --git_args '--jobs 4' static_string
- name: Bootstrap b2
run: .\bootstrap.bat
shell: cmd
- name: Generate Boost headers
run: .\b2 headers
shell: cmd
- name: Build and run example tests
run: |
.\b2 libs/static_string/example/static_cstring ^
toolset=${{ matrix.toolset }} ^
cxxstd=${{ matrix.cxxstd }} ^
variant=debug,release ^
address-model=64
shell: cmd
+34 -30
View File
@@ -13,6 +13,7 @@
#include <boost/static_string/config.hpp>
#include <algorithm>
#include <climits>
#include <compare>
#include <cstddef>
#include <ostream>
#include <string>
@@ -25,7 +26,7 @@ namespace static_strings {
namespace detail {
// Primary template: No remaining-capacity trick; uses traits::length() for size.
// Primary template: No remaining-capacity trick; uses traits::length() for length.
template<std::size_t N, typename CharT, typename Traits, bool UseRemaining>
class static_cstring_base
{
@@ -45,18 +46,9 @@ public:
{
data_[sz] = value_type{};
}
constexpr void init_empty() noexcept
{
data_[0] = value_type{};
}
// Defaulted comparisons for structural type support.
constexpr bool operator==(const static_cstring_base&) const noexcept = default;
constexpr auto operator<=>(const static_cstring_base&) const noexcept = default;
};
// Specialization for N <= UCHAR_MAX: Use remaining-capacity trick.
// Specialization for N <= UCHAR_MAX: Uses remaining-capacity trick.
template<std::size_t N, typename CharT, typename Traits>
class static_cstring_base<N, CharT, Traits, true>
{
@@ -77,16 +69,6 @@ public:
data_[sz] = value_type{};
data_[N] = static_cast<value_type>(N - sz);
}
constexpr void init_empty() noexcept
{
data_[0] = value_type{};
data_[N] = static_cast<value_type>(N);
}
// Defaulted comparisons for structural type support.
constexpr bool operator==(const static_cstring_base&) const noexcept = default;
constexpr auto operator<=>(const static_cstring_base&) const noexcept = default;
};
} // namespace detail
@@ -100,7 +82,6 @@ public:
using base::data_;
using base::get_size;
using base::set_size;
using base::init_empty;
// Member types
using traits_type = Traits;
@@ -120,7 +101,7 @@ public:
// Constructors.
constexpr basic_static_cstring() noexcept
{
init_empty();
set_size(0);
}
constexpr basic_static_cstring(const CharT* s)
@@ -141,8 +122,8 @@ public:
template<std::size_t M>
constexpr basic_static_cstring(const CharT (&arr)[M])
{
static_assert(M <= N + 1, "String literal too long for static_cstring");
assign(arr, M - 1);
static_assert(M <= N + 1, "Array too big for static_cstring");
assign(arr);
}
constexpr size_type size() const noexcept
@@ -201,21 +182,25 @@ public:
constexpr reference front() noexcept
{
BOOST_STATIC_STRING_ASSERT(!empty());
return data_[0];
}
constexpr const_reference front() const noexcept
{
BOOST_STATIC_STRING_ASSERT(!empty());
return data_[0];
}
constexpr reference back() noexcept
{
BOOST_STATIC_STRING_ASSERT(!empty());
return data_[size() - 1];
}
constexpr const_reference back() const noexcept
{
BOOST_STATIC_STRING_ASSERT(!empty());
return data_[size() - 1];
}
@@ -268,7 +253,7 @@ public:
// Modifiers.
constexpr void clear() noexcept
{
init_empty();
set_size(0);
}
constexpr basic_static_cstring& assign(const CharT* s)
@@ -378,7 +363,17 @@ public:
constexpr int compare(const CharT* s) const noexcept
{
return compare(basic_static_cstring(s));
const size_type lhs_sz = size();
const size_type rhs_sz = traits_type::length(s);
const int result = traits_type::compare(data_, s, (std::min)(lhs_sz, rhs_sz));
return result != 0
? result
: lhs_sz < rhs_sz
? -1
: lhs_sz > rhs_sz
? 1
: 0;
}
// Conversions.
@@ -400,9 +395,18 @@ public:
other = tmp;
}
// Defaulted comparisons for structural type (C++20).
constexpr bool operator==(const basic_static_cstring&) const noexcept = default;
constexpr auto operator<=>(const basic_static_cstring&) const noexcept = default;
constexpr bool operator==(const basic_static_cstring& other) const noexcept
{
const size_type sz = size();
return sz == other.size()
&& traits_type::compare(data_, other.data_, sz) == 0;
}
constexpr std::strong_ordering
operator<=>(const basic_static_cstring& other) const noexcept
{
return compare(other) <=> 0;
}
};
#if defined(BOOST_STATIC_STRING_USE_DEDUCT)
+125 -20
View File
@@ -75,41 +75,39 @@ testCStringRemainingCapacityTrick()
large.assign(100, 'x');
BOOST_TEST(large.size() == 100);
BOOST_TEST(large.capacity() == UCHAR_MAX);
BOOST_TEST(static_cast<unsigned char>(large.data()[UCHAR_MAX]) == (large.capacity() - large.size()));
BOOST_TEST(static_cast<unsigned char>(large.data()[UCHAR_MAX]) == (UCHAR_MAX - large.size()));
}
}
template<typename S>
struct CStringTypeTraits
{
static_assert(std::is_trivially_copyable<S>::value);
static_assert(std::is_trivially_copy_constructible<S>::value);
static_assert(std::is_trivially_move_constructible<S>::value);
static_assert(std::is_trivially_copy_assignable<S>::value);
static_assert(std::is_trivially_move_assignable<S>::value);
static_assert(std::is_trivially_destructible<S>::value);
};
static
void
testCStringTypeTraits()
{
// static_cstring should be trivially copyable for use in POD structs.
{
using S = static_cstring<0>;
static_assert(std::is_trivially_copyable<S>::value, "");
static_assert(std::is_trivially_copy_constructible<S>::value, "");
static_assert(std::is_trivially_move_constructible<S>::value, "");
static_assert(std::is_trivially_copy_assignable<S>::value, "");
static_assert(std::is_trivially_move_assignable<S>::value, "");
static_assert(std::is_trivially_destructible<S>::value, "");
CStringTypeTraits< S > check;
static_cast<void>(check);
}
{
using S = static_cstring<63>;
static_assert(std::is_trivially_copyable<S>::value, "");
static_assert(std::is_trivially_copy_constructible<S>::value, "");
static_assert(std::is_trivially_move_constructible<S>::value, "");
static_assert(std::is_trivially_copy_assignable<S>::value, "");
static_assert(std::is_trivially_move_assignable<S>::value, "");
static_assert(std::is_trivially_destructible<S>::value, "");
CStringTypeTraits< S > check;
static_cast<void>(check);
}
{
using S = static_cstring<300>;
static_assert(std::is_trivially_copyable<S>::value, "");
static_assert(std::is_trivially_copy_constructible<S>::value, "");
static_assert(std::is_trivially_move_constructible<S>::value, "");
static_assert(std::is_trivially_copy_assignable<S>::value, "");
static_assert(std::is_trivially_move_assignable<S>::value, "");
static_assert(std::is_trivially_destructible<S>::value, "");
CStringTypeTraits< S > check;
static_cast<void>(check);
}
}
@@ -156,6 +154,13 @@ testCStringConstruct()
BOOST_TEST(*s1.end() == 0);
}
// Construct from a C string with embedded NULs.
{
const char arr[] = { '1', '2', '\0', '4', '5', '\0' };
static_cstring<5> s1(arr);
BOOST_TEST(s1 == "12" );
}
// Copy construction.
{
static_cstring<5> s1("12345");
@@ -445,6 +450,105 @@ testCStringComparison()
BOOST_TEST(s.compare("abd") < 0);
BOOST_TEST(s.compare("abb") > 0);
}
// compare(const CharT*) must not throw when the argument is longer
// than the static capacity.
{
static_cstring<3> s("abc");
BOOST_TEST(s.compare("abcd") < 0);
BOOST_TEST(s.compare("abcdefghijklmnop") < 0);
BOOST_TEST(s.compare("abb") > 0);
BOOST_TEST(s.compare("abd") < 0);
BOOST_TEST(s.compare("abc") == 0);
BOOST_TEST(s.compare("ab") > 0);
BOOST_TEST(s.compare("") > 0);
}
// Same via operator== / operator!= with an over-long C string.
{
static_cstring<3> s("abc");
BOOST_TEST(!(s == "abcd"));
BOOST_TEST(s != "abcd");
BOOST_TEST(!("abcd" == s));
BOOST_TEST("abcd" != s);
}
// Empty static_cstring vs non-empty C string.
{
static_cstring<5> empty;
BOOST_TEST(empty.compare("hello") < 0);
BOOST_TEST(empty.compare("") == 0);
}
}
static
void
testCStringComparisonAfterShrink()
{
// Two semantically equal strings must compare equal, even if one of
// them was first longer and then shrunk.
// Small-N path (remaining-capacity trick): shrinking assign(s, n).
{
static_cstring<10> s("hello");
s.assign("hi", 2);
static_cstring<10> fresh("hi");
BOOST_TEST(s == fresh);
BOOST_TEST(!(s != fresh));
BOOST_TEST(!(s < fresh));
BOOST_TEST(!(s > fresh));
BOOST_TEST(s <= fresh);
BOOST_TEST(s >= fresh);
}
// Small-N path: shrinking assign(count, ch).
{
static_cstring<10> s("xxxxx");
s.assign(2, 'y');
static_cstring<10> fresh(2, 'y');
BOOST_TEST(s == fresh);
}
// Small-N path: shrinking via operator=(const CharT*).
{
static_cstring<10> s("hello");
s = "hi";
static_cstring<10> fresh("hi");
BOOST_TEST(s == fresh);
}
// Small-N path: shrinking via pop_back().
{
static_cstring<10> s("abcde");
s.pop_back();
static_cstring<10> fresh("abcd");
BOOST_TEST(s == fresh);
}
// Small-N path: shrinking via clear().
{
static_cstring<10> s("abcde");
s.clear();
static_cstring<10> fresh;
BOOST_TEST(s == fresh);
}
// Large-N path (N > UCHAR_MAX, no remaining-capacity trick).
{
static_cstring<300> s;
s.assign(200, 'a');
s.assign(50, 'b');
static_cstring<300> fresh(50, 'b');
BOOST_TEST(s == fresh);
BOOST_TEST(!(s < fresh));
BOOST_TEST(!(s > fresh));
}
}
static
@@ -652,6 +756,7 @@ runTests()
testCStringPushPop();
testCStringAppend();
testCStringComparison();
testCStringComparisonAfterShrink();
testCStringConversion();
testCStringStream();
testCStringSwap();
@@ -2314,6 +2314,23 @@ public:
return N;
}
/** Return the number of additional characters that can be stored.
Returns `max_size() - size()`, i.e. the number of characters
that can still be inserted before the string reaches its
capacity.
@par Complexity
Constant.
*/
BOOST_STATIC_STRING_CPP11_CONSTEXPR
size_type
available() const noexcept
{
return max_size() - size();
}
/** Increase the capacity.
This function has no effect.
+2 -3
View File
@@ -72,9 +72,8 @@ struct cxper_char_traits
// This implementation does not handle overlapping ranges where
// dest > src. A correct implementation would need to detect this
// case and copy backwards, but detecting overlap requires pointer
// comparisons that are not allowed in constant expressions when
// the pointers point to unrelated objects (e.g., a string literal
// and the static_string's internal buffer).
// comparisons that many of the tested compilers (incorrectly) refuse
// in constant expressions.
//
// Since cxper_char_traits is only used for testing constexpr
// functionality and the tests do not exercise overlapping moves
+23
View File
@@ -989,6 +989,29 @@ testCapacity()
BOOST_TEST(static_string<3>{"abc"}.max_size() == 3);
BOOST_TEST(static_string<5>{"abc"}.max_size() == 5);
// available()
BOOST_TEST(static_string<0>{}.available() == 0);
BOOST_TEST(static_string<1>{}.available() == 1);
BOOST_TEST(static_string<1>{"a"}.available() == 0);
BOOST_TEST(static_string<3>{"abc"}.available() == 0);
BOOST_TEST(static_string<5>{"abc"}.available() == 2);
BOOST_TEST(static_string<5>{}.available() == 5);
{
static_string<8> s("hi");
BOOST_TEST(s.available() == 6);
s.append(" there");
BOOST_TEST(s.available() == 0);
s.clear();
BOOST_TEST(s.available() == 8);
}
// Intended use with the pos/count-taking overloads.
{
static_string<5> s("ab");
s.append(string_view{"xyzwv"}, 0, s.available());
BOOST_TEST(s == "abxyz");
BOOST_TEST(s.available() == 0);
}
// reserve(std::size_t n)
static_string<3>{}.reserve(0);
static_string<3>{}.reserve(1);