From 153659d4d3663ffa8243e22fb1a706e91ed76b24 Mon Sep 17 00:00:00 2001 From: Thomas Willson Date: Thu, 4 Jun 2026 13:12:58 -0700 Subject: [PATCH] Add support for C++17. --- .github/workflows/build-linux-clang.yml | 13 ++-- .github/workflows/build-linux-gcc.yml | 13 ++-- .github/workflows/build-windows-mingw.yml | 13 ++-- .github/workflows/build-windows-msvc.yml | 14 ++-- CMakeLists.txt | 6 ++ README.md | 24 +++++-- benchmark/BM_Comparative.cpp | 8 ++- benchmark/CMakeLists.txt | 2 +- cmake/nfxStringBuilderBuildConfig.cmake | 13 ++++ cmake/nfxStringBuilderTargets.cmake | 8 ++- include/nfx/detail/string/StringBuilder.inl | 80 +++++++++++---------- include/nfx/string/StringBuilder.h | 49 ++++++++++++- samples/CMakeLists.txt | 2 +- samples/Sample_StringBuilder.cpp | 17 ++++- src/StringBuilder.cpp | 18 ++--- test/CMakeLists.txt | 2 +- test/Tests_StringBuilder.cpp | 9 ++- 17 files changed, 213 insertions(+), 78 deletions(-) diff --git a/.github/workflows/build-linux-clang.yml b/.github/workflows/build-linux-clang.yml index f343495..09e7d93 100644 --- a/.github/workflows/build-linux-clang.yml +++ b/.github/workflows/build-linux-clang.yml @@ -23,7 +23,11 @@ permissions: jobs: build-linux-clang: runs-on: ubuntu-latest - name: Ubuntu Clang 18 + name: Ubuntu Clang 18 (C++${{ matrix.std }}) + strategy: + fail-fast: false + matrix: + std: [17, 20] steps: - name: Checkout repository @@ -37,9 +41,9 @@ jobs: path: | build ~/.cache/ccache - key: ${{ runner.os }}-clang18-ninja-${{ hashFiles('**/CMakeLists.txt', 'include/**') }} + key: ${{ runner.os }}-clang18-cxx${{ matrix.std }}-ninja-${{ hashFiles('**/CMakeLists.txt', 'include/**') }} restore-keys: | - ${{ runner.os }}-clang18-ninja- + ${{ runner.os }}-clang18-cxx${{ matrix.std }}-ninja- - name: Set up build environment run: | @@ -65,6 +69,7 @@ jobs: cmake -B build \ -G "Ninja" \ -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} \ + -DNFX_STRINGBUILDER_CXX_STANDARD=${{ matrix.std }} \ -DNFX_STRINGBUILDER_BUILD_STATIC=ON \ -DNFX_STRINGBUILDER_BUILD_SHARED=ON \ -DNFX_STRINGBUILDER_BUILD_TESTS=ON \ @@ -83,7 +88,7 @@ jobs: if: failure() uses: actions/upload-artifact@v4 with: - name: test-results-linux-clang18 + name: test-results-linux-clang18-cxx${{ matrix.std }} path: | build/Testing/Temporary/LastTest.log build/Testing/Temporary/LastTestsFailed.log diff --git a/.github/workflows/build-linux-gcc.yml b/.github/workflows/build-linux-gcc.yml index bff649c..aead165 100644 --- a/.github/workflows/build-linux-gcc.yml +++ b/.github/workflows/build-linux-gcc.yml @@ -23,7 +23,11 @@ permissions: jobs: build-linux-gcc: runs-on: ubuntu-latest - name: Ubuntu GCC 14 + name: Ubuntu GCC 14 (C++${{ matrix.std }}) + strategy: + fail-fast: false + matrix: + std: [17, 20] steps: - name: Checkout repository @@ -37,9 +41,9 @@ jobs: path: | build ~/.cache/ccache - key: ${{ runner.os }}-gcc14-ninja-${{ hashFiles('**/CMakeLists.txt', 'include/**') }} + key: ${{ runner.os }}-gcc14-cxx${{ matrix.std }}-ninja-${{ hashFiles('**/CMakeLists.txt', 'include/**') }} restore-keys: | - ${{ runner.os }}-gcc14-ninja- + ${{ runner.os }}-gcc14-cxx${{ matrix.std }}-ninja- - name: Set up build environment run: | @@ -65,6 +69,7 @@ jobs: cmake -B build \ -G "Ninja" \ -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} \ + -DNFX_STRINGBUILDER_CXX_STANDARD=${{ matrix.std }} \ -DNFX_STRINGBUILDER_BUILD_STATIC=ON \ -DNFX_STRINGBUILDER_BUILD_SHARED=ON \ -DNFX_STRINGBUILDER_BUILD_TESTS=ON \ @@ -83,7 +88,7 @@ jobs: if: failure() uses: actions/upload-artifact@v4 with: - name: test-results-linux-gcc14 + name: test-results-linux-gcc14-cxx${{ matrix.std }} path: | build/Testing/Temporary/LastTest.log build/Testing/Temporary/LastTestsFailed.log diff --git a/.github/workflows/build-windows-mingw.yml b/.github/workflows/build-windows-mingw.yml index ec48dcd..5be3255 100644 --- a/.github/workflows/build-windows-mingw.yml +++ b/.github/workflows/build-windows-mingw.yml @@ -23,7 +23,11 @@ permissions: jobs: build-windows-mingw: runs-on: windows-2022 - name: Windows MinGW + name: Windows MinGW (C++${{ matrix.std }}) + strategy: + fail-fast: false + matrix: + std: [17, 20] steps: - name: Checkout repository @@ -37,9 +41,9 @@ jobs: path: | build ${{ runner.temp }}/ccache - key: ${{ runner.os }}-mingw-ninja-${{ hashFiles('**/CMakeLists.txt', 'include/**') }} + key: ${{ runner.os }}-mingw-cxx${{ matrix.std }}-ninja-${{ hashFiles('**/CMakeLists.txt', 'include/**') }} restore-keys: | - ${{ runner.os }}-mingw-ninja- + ${{ runner.os }}-mingw-cxx${{ matrix.std }}-ninja- - name: Set up MinGW uses: msys2/setup-msys2@v2 @@ -72,6 +76,7 @@ jobs: cmake -B build \ -G "Ninja" \ -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} \ + -DNFX_STRINGBUILDER_CXX_STANDARD=${{ matrix.std }} \ -DNFX_STRINGBUILDER_BUILD_STATIC=ON \ -DNFX_STRINGBUILDER_BUILD_SHARED=ON \ -DNFX_STRINGBUILDER_BUILD_TESTS=ON \ @@ -94,7 +99,7 @@ jobs: if: failure() uses: actions/upload-artifact@v4 with: - name: test-results-windows-mingw + name: test-results-windows-mingw-cxx${{ matrix.std }} path: | build/Testing/Temporary/LastTest.log build/Testing/Temporary/LastTestsFailed.log diff --git a/.github/workflows/build-windows-msvc.yml b/.github/workflows/build-windows-msvc.yml index 4b31f79..75427e5 100644 --- a/.github/workflows/build-windows-msvc.yml +++ b/.github/workflows/build-windows-msvc.yml @@ -23,7 +23,11 @@ permissions: jobs: build-windows-msvc: runs-on: windows-2022 - name: Windows MSVC 2022 + name: Windows MSVC 2022 (C++${{ matrix.std }}) + strategy: + fail-fast: false + matrix: + std: [17, 20] steps: - name: Checkout repository @@ -37,12 +41,12 @@ jobs: path: | build C:\Users\runneradmin\AppData\Local\Microsoft\MSBuild - key: ${{ runner.os }}-msvc2022-vs2022-${{ hashFiles('**/CMakeLists.txt', 'include/**') }} + key: ${{ runner.os }}-msvc2022-cxx${{ matrix.std }}-vs2022-${{ hashFiles('**/CMakeLists.txt', 'include/**') }} restore-keys: | - ${{ runner.os }}-msvc2022-vs2022- + ${{ runner.os }}-msvc2022-cxx${{ matrix.std }}-vs2022- - name: Configure CMake - run: cmake -B build -G "Visual Studio 17 2022" -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} -DNFX_STRINGBUILDER_BUILD_STATIC=ON -DNFX_STRINGBUILDER_BUILD_SHARED=ON -DNFX_STRINGBUILDER_BUILD_TESTS=ON -DNFX_STRINGBUILDER_BUILD_SAMPLES=ON -DNFX_STRINGBUILDER_BUILD_BENCHMARKS=ON -DNFX_STRINGBUILDER_BUILD_DOCUMENTATION=OFF + run: cmake -B build -G "Visual Studio 17 2022" -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} -DNFX_STRINGBUILDER_CXX_STANDARD=${{ matrix.std }} -DNFX_STRINGBUILDER_BUILD_STATIC=ON -DNFX_STRINGBUILDER_BUILD_SHARED=ON -DNFX_STRINGBUILDER_BUILD_TESTS=ON -DNFX_STRINGBUILDER_BUILD_SAMPLES=ON -DNFX_STRINGBUILDER_BUILD_BENCHMARKS=ON -DNFX_STRINGBUILDER_BUILD_DOCUMENTATION=OFF - name: Build with MSVC run: | @@ -61,7 +65,7 @@ jobs: if: failure() uses: actions/upload-artifact@v4 with: - name: test-results-windows-msvc2022 + name: test-results-windows-msvc2022-cxx${{ matrix.std }} path: | build/Testing/Temporary/LastTest.log build/Testing/Temporary/LastTestsFailed.log diff --git a/CMakeLists.txt b/CMakeLists.txt index 135ce3b..01910e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,12 @@ enable_testing() option(NFX_STRINGBUILDER_BUILD_STATIC "Build static library" ON ) option(NFX_STRINGBUILDER_BUILD_SHARED "Build shared library" OFF) +# --- Language standard --- +# Minimum required standard is C++17. C++20 additionally enables the +# std::format()-based formatting API and the std::formatter specialization. +set(NFX_STRINGBUILDER_CXX_STANDARD "20" CACHE STRING "C++ standard used to build nfx-stringbuilder (17 or 20)") +set_property(CACHE NFX_STRINGBUILDER_CXX_STANDARD PROPERTY STRINGS "17" "20") + # --- Build components --- option(NFX_STRINGBUILDER_BUILD_TESTS "Build tests" OFF) option(NFX_STRINGBUILDER_BUILD_SAMPLES "Build samples" OFF) diff --git a/README.md b/README.md index 063959b..f94814d 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,17 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://github.com/nfx-libs/nfx-stringbuilder/blob/main/LICENSE) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/nfx-libs/nfx-stringbuilder?style=flat-square)](https://github.com/nfx-libs/nfx-stringbuilder/releases) [![GitHub tag (latest by date)](https://img.shields.io/github/tag/nfx-libs/nfx-stringbuilder?style=flat-square)](https://github.com/nfx-libs/nfx-stringbuilder/tags)
-![C++20](https://img.shields.io/badge/C%2B%2B-20-blue?style=flat-square) ![CMake](https://img.shields.io/badge/CMake-3.20%2B-green.svg?style=flat-square) ![Cross Platform](https://img.shields.io/badge/Platform-Linux_Windows-lightgrey?style=flat-square) +![C++17/20](https://img.shields.io/badge/C%2B%2B-17%2F20-blue?style=flat-square) ![CMake](https://img.shields.io/badge/CMake-3.20%2B-green.svg?style=flat-square) ![Cross Platform](https://img.shields.io/badge/Platform-Linux_Windows-lightgrey?style=flat-square) [![Linux GCC](https://img.shields.io/github/actions/workflow/status/nfx-libs/nfx-stringbuilder/build-linux-gcc.yml?branch=main&label=Linux%20GCC&style=flat-square)](https://github.com/nfx-libs/nfx-stringbuilder/actions/workflows/build-linux-gcc.yml) [![Linux Clang](https://img.shields.io/github/actions/workflow/status/nfx-libs/nfx-stringbuilder/build-linux-clang.yml?branch=main&label=Linux%20Clang&style=flat-square)](https://github.com/nfx-libs/nfx-stringbuilder/actions/workflows/build-linux-clang.yml) [![Windows MinGW](https://img.shields.io/github/actions/workflow/status/nfx-libs/nfx-stringbuilder/build-windows-mingw.yml?branch=main&label=Windows%20MinGW&style=flat-square)](https://github.com/nfx-libs/nfx-stringbuilder/actions/workflows/build-windows-mingw.yml) [![Windows MSVC](https://img.shields.io/github/actions/workflow/status/nfx-libs/nfx-stringbuilder/build-windows-msvc.yml?branch=main&label=Windows%20MSVC&style=flat-square)](https://github.com/nfx-libs/nfx-stringbuilder/actions/workflows/build-windows-msvc.yml) [![CodeQL](https://img.shields.io/github/actions/workflow/status/nfx-libs/nfx-stringbuilder/codeql.yml?branch=main&label=CodeQL&style=flat-square)](https://github.com/nfx-libs/nfx-stringbuilder/actions/workflows/codeql.yml) -> A cross-platform C++20 high-performance string builder with Small Buffer Optimization and efficient memory management +> A cross-platform C++17/C++20 high-performance string builder with Small Buffer Optimization and efficient memory management ## Overview -**nfx-stringbuilder** is a modern C++20 library providing efficient string building capabilities with zero heap allocations for small strings (≤256 bytes). Designed for applications requiring high-performance string concatenation with minimal allocations, it features Small Buffer Optimization (SBO), comprehensive type support, and C++20 std::format integration. +**nfx-stringbuilder** is a modern C++ library providing efficient string building capabilities with zero heap allocations for small strings (≤256 bytes). Designed for applications requiring high-performance string concatenation with minimal allocations, it features Small Buffer Optimization (SBO) and comprehensive type support. It builds with **C++17 or C++20**: the core API is fully available under C++17, while C++20 additionally enables the `std::format`-based `format()` method and the `std::formatter` specialization. ## Key Features @@ -23,8 +23,8 @@ - **Fluent API**: Method chaining and stream operators (`<<`) for natural concatenation - **Variadic append()**: Batch multiple arguments in a single call for optimal performance - **Type Support**: Strings, string_view, C-strings, characters, and numeric types (int8/16/32/64, uint8/16/32/64, float, double) -- **C++20 std::format Integration**: Template `format()` method for modern formatting -- **std::formatter Specializations**: Zero-copy integration with `std::format` for StringBuilder +- **C++20 std::format Integration**: Template `format()` method for modern formatting *(requires C++20)* +- **std::formatter Specializations**: Zero-copy integration with `std::format` for StringBuilder *(requires C++20)* - **Capacity Hints**: Pre-allocate buffers with constructor parameter for optimal performance - **Direct Buffer Access**: High-performance operations without wrappers - **Iterator Support**: Range-based for loops and STL algorithms @@ -57,12 +57,21 @@ ### Requirements -- C++20 compatible compiler: +- C++17 or C++20 compatible compiler: - **GCC 14+** (14.2.0 tested) - **Clang 18+** (19.1.7 tested) - **MSVC 2022+** (19.44+ tested) - CMake 3.20 or higher +> **C++ standard:** The library defaults to building with C++20. Select the +> standard explicitly with `-DNFX_STRINGBUILDER_CXX_STANDARD=17` or `=20`. +> Under C++17 the entire API is available except the `std::format`-based +> `format()` method and the `std::formatter` specialization, +> which require C++20. The exported `nfx-stringbuilder::static` and +> `nfx-stringbuilder::nfx-stringbuilder` targets propagate a minimum +> requirement of C++17 to consumers, who may compile at a higher standard than +> the library was built with. + ### CMake Integration ```cmake @@ -70,6 +79,9 @@ option(NFX_STRINGBUILDER_BUILD_STATIC "Build static library" ON ) option(NFX_STRINGBUILDER_BUILD_SHARED "Build shared library" OFF) +# --- Language standard (17 or 20; C++20 enables std::format APIs) --- +set(NFX_STRINGBUILDER_CXX_STANDARD "20" CACHE STRING "C++ standard used to build nfx-stringbuilder (17 or 20)") + # --- Build components --- option(NFX_STRINGBUILDER_BUILD_TESTS "Build tests" OFF) option(NFX_STRINGBUILDER_BUILD_SAMPLES "Build samples" OFF) diff --git a/benchmark/BM_Comparative.cpp b/benchmark/BM_Comparative.cpp index d257dda..59c1750 100644 --- a/benchmark/BM_Comparative.cpp +++ b/benchmark/BM_Comparative.cpp @@ -42,7 +42,9 @@ # include #endif -#include +#if defined( NFX_STRINGBUILDER_HAS_STD_FORMAT ) +# include +#endif #include namespace nfx::string::benchmark @@ -545,9 +547,11 @@ namespace nfx::string::benchmark for( auto _ : state ) { (void)_; +#if defined( NFX_STRINGBUILDER_HAS_STD_FORMAT ) std::string result = std::format( "User {} (ID: {}) scored {:.2f} points at {}", "Alice", 42, 95.75, "2024-01-15 10:30:00" ); ::benchmark::DoNotOptimize( result ); +#endif } } @@ -582,11 +586,13 @@ namespace nfx::string::benchmark for( auto _ : state ) { (void)_; +#if defined( NFX_STRINGBUILDER_HAS_STD_FORMAT ) StringBuilder builder; builder.append( std::format( "User {} (ID: {}) scored {:.2f} points at {}", "Alice", 42, 95.75, "2024-01-15 10:30:00" ) ); std::string result = builder.toString(); ::benchmark::DoNotOptimize( result ); +#endif } } diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index 8b5b69e..107a331 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -211,7 +211,7 @@ foreach(benchmark_source ${benchmark_sources}) set_target_properties(${benchmark_target_name} PROPERTIES - CXX_STANDARD 20 + CXX_STANDARD ${NFX_STRINGBUILDER_CXX_STANDARD} CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF POSITION_INDEPENDENT_CODE ON diff --git a/cmake/nfxStringBuilderBuildConfig.cmake b/cmake/nfxStringBuilderBuildConfig.cmake index 422ddae..5fc0c1d 100644 --- a/cmake/nfxStringBuilderBuildConfig.cmake +++ b/cmake/nfxStringBuilderBuildConfig.cmake @@ -30,6 +30,19 @@ if(NOT NFX_STRINGBUILDER_BUILD_STATIC AND NOT NFX_STRINGBUILDER_BUILD_SHARED) endif() endif() +# --- Validate the requested C++ standard --- +if(NOT NFX_STRINGBUILDER_CXX_STANDARD MATCHES "^(17|20)$") + message(FATAL_ERROR + "NFX_STRINGBUILDER_CXX_STANDARD must be either 17 or 20 " + "(got '${NFX_STRINGBUILDER_CXX_STANDARD}').") +endif() + +if(NFX_STRINGBUILDER_CXX_STANDARD STREQUAL "17") + message(STATUS "nfx-stringbuilder: Building with C++17 (std::format API disabled)") +else() + message(STATUS "nfx-stringbuilder: Building with C++${NFX_STRINGBUILDER_CXX_STANDARD}") +endif() + #---------------------------------------------- # Multi-config generator setup #---------------------------------------------- diff --git a/cmake/nfxStringBuilderTargets.cmake b/cmake/nfxStringBuilderTargets.cmake index 9c7ce55..4af43d9 100644 --- a/cmake/nfxStringBuilderTargets.cmake +++ b/cmake/nfxStringBuilderTargets.cmake @@ -65,7 +65,7 @@ function(configure_target target_name) # --- Properties --- set_target_properties(${target_name} PROPERTIES - CXX_STANDARD 20 + CXX_STANDARD ${NFX_STRINGBUILDER_CXX_STANDARD} CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF DEBUG_POSTFIX "-d" @@ -74,6 +74,12 @@ function(configure_target target_name) SOVERSION ${PROJECT_VERSION_MAJOR} ) + # --- Public language requirement --- + # Propagate a minimum of C++17 to consumers of the static/shared targets. + # The headers compile under both C++17 and C++20; consumers may build at a + # higher standard than the library was compiled with. + target_compile_features(${target_name} PUBLIC cxx_std_17) + # --- Enable specific CPU features --- if(NFX_STRINGBUILDER_ENABLE_SIMD) include(CheckCXXSourceCompiles) diff --git a/include/nfx/detail/string/StringBuilder.inl b/include/nfx/detail/string/StringBuilder.inl index 3393d04..74db4dc 100644 --- a/include/nfx/detail/string/StringBuilder.inl +++ b/include/nfx/detail/string/StringBuilder.inl @@ -112,7 +112,7 @@ namespace nfx::string inline void StringBuilder::reserve( size_t newCapacity ) { - if( newCapacity > m_capacity ) [[likely]] + if( newCapacity > m_capacity ) NFX_STRINGBUILDER_LIKELY { ensureCapacity( newCapacity ); } @@ -135,7 +135,7 @@ namespace nfx::string inline StringBuilder& StringBuilder::append( std::string_view str ) { - if( str.empty() ) [[unlikely]] + if( str.empty() ) NFX_STRINGBUILDER_UNLIKELY { return *this; } @@ -143,7 +143,7 @@ namespace nfx::string const size_t len = str.size(); const size_t newSize = m_size + len; - if( newSize > m_capacity ) [[unlikely]] + if( newSize > m_capacity ) NFX_STRINGBUILDER_UNLIKELY { ensureCapacity( newSize ); } @@ -156,7 +156,7 @@ namespace nfx::string inline StringBuilder& StringBuilder::append( const char* str ) { - if( str ) [[likely]] + if( str ) NFX_STRINGBUILDER_LIKELY { return append( std::string_view{ str, strlen( str ) } ); } @@ -172,7 +172,7 @@ namespace nfx::string inline StringBuilder& StringBuilder::append( char c ) { - if( m_size + 1 > m_capacity ) [[unlikely]] + if( m_size + 1 > m_capacity ) NFX_STRINGBUILDER_UNLIKELY { ensureCapacity( m_size + 1 ); } @@ -182,12 +182,12 @@ namespace nfx::string inline StringBuilder& StringBuilder::append( std::int8_t value ) { - if( m_size + INT8_MAX_DIGITS > m_capacity ) [[unlikely]] + if( m_size + INT8_MAX_DIGITS > m_capacity ) NFX_STRINGBUILDER_UNLIKELY { ensureCapacity( m_size + INT8_MAX_DIGITS ); } auto [ptr, ec] = std::to_chars( m_buffer + m_size, m_buffer + m_capacity, static_cast( value ) ); - if( ec == std::errc() ) [[likely]] + if( ec == std::errc() ) NFX_STRINGBUILDER_LIKELY { m_size = ptr - m_buffer; } @@ -196,12 +196,12 @@ namespace nfx::string inline StringBuilder& StringBuilder::append( std::uint8_t value ) { - if( m_size + UINT8_MAX_DIGITS > m_capacity ) [[unlikely]] + if( m_size + UINT8_MAX_DIGITS > m_capacity ) NFX_STRINGBUILDER_UNLIKELY { ensureCapacity( m_size + UINT8_MAX_DIGITS ); } auto [ptr, ec] = std::to_chars( m_buffer + m_size, m_buffer + m_capacity, static_cast( value ) ); - if( ec == std::errc() ) [[likely]] + if( ec == std::errc() ) NFX_STRINGBUILDER_LIKELY { m_size = ptr - m_buffer; } @@ -210,12 +210,12 @@ namespace nfx::string inline StringBuilder& StringBuilder::append( std::int16_t value ) { - if( m_size + INT16_MAX_DIGITS > m_capacity ) [[unlikely]] + if( m_size + INT16_MAX_DIGITS > m_capacity ) NFX_STRINGBUILDER_UNLIKELY { ensureCapacity( m_size + INT16_MAX_DIGITS ); } auto [ptr, ec] = std::to_chars( m_buffer + m_size, m_buffer + m_capacity, value ); - if( ec == std::errc() ) [[likely]] + if( ec == std::errc() ) NFX_STRINGBUILDER_LIKELY { m_size = ptr - m_buffer; } @@ -224,12 +224,12 @@ namespace nfx::string inline StringBuilder& StringBuilder::append( std::uint16_t value ) { - if( m_size + UINT16_MAX_DIGITS > m_capacity ) [[unlikely]] + if( m_size + UINT16_MAX_DIGITS > m_capacity ) NFX_STRINGBUILDER_UNLIKELY { ensureCapacity( m_size + UINT16_MAX_DIGITS ); } auto [ptr, ec] = std::to_chars( m_buffer + m_size, m_buffer + m_capacity, value ); - if( ec == std::errc() ) [[likely]] + if( ec == std::errc() ) NFX_STRINGBUILDER_LIKELY { m_size = ptr - m_buffer; } @@ -238,12 +238,12 @@ namespace nfx::string inline StringBuilder& StringBuilder::append( std::int32_t value ) { - if( m_size + INT32_MAX_DIGITS > m_capacity ) [[unlikely]] + if( m_size + INT32_MAX_DIGITS > m_capacity ) NFX_STRINGBUILDER_UNLIKELY { ensureCapacity( m_size + INT32_MAX_DIGITS ); } auto [ptr, ec] = std::to_chars( m_buffer + m_size, m_buffer + m_capacity, value ); - if( ec == std::errc() ) [[likely]] + if( ec == std::errc() ) NFX_STRINGBUILDER_LIKELY { m_size = ptr - m_buffer; } @@ -252,12 +252,12 @@ namespace nfx::string inline StringBuilder& StringBuilder::append( std::uint32_t value ) { - if( m_size + UINT32_MAX_DIGITS > m_capacity ) [[unlikely]] + if( m_size + UINT32_MAX_DIGITS > m_capacity ) NFX_STRINGBUILDER_UNLIKELY { ensureCapacity( m_size + UINT32_MAX_DIGITS ); } auto [ptr, ec] = std::to_chars( m_buffer + m_size, m_buffer + m_capacity, value ); - if( ec == std::errc() ) [[likely]] + if( ec == std::errc() ) NFX_STRINGBUILDER_LIKELY { m_size = ptr - m_buffer; } @@ -266,12 +266,12 @@ namespace nfx::string inline StringBuilder& StringBuilder::append( std::int64_t value ) { - if( m_size + INT64_MAX_DIGITS > m_capacity ) [[unlikely]] + if( m_size + INT64_MAX_DIGITS > m_capacity ) NFX_STRINGBUILDER_UNLIKELY { ensureCapacity( m_size + INT64_MAX_DIGITS ); } auto [ptr, ec] = std::to_chars( m_buffer + m_size, m_buffer + m_capacity, value ); - if( ec == std::errc() ) [[likely]] + if( ec == std::errc() ) NFX_STRINGBUILDER_LIKELY { m_size = ptr - m_buffer; } @@ -280,12 +280,12 @@ namespace nfx::string inline StringBuilder& StringBuilder::append( std::uint64_t value ) { - if( m_size + UINT64_MAX_DIGITS > m_capacity ) [[unlikely]] + if( m_size + UINT64_MAX_DIGITS > m_capacity ) NFX_STRINGBUILDER_UNLIKELY { ensureCapacity( m_size + UINT64_MAX_DIGITS ); } auto [ptr, ec] = std::to_chars( m_buffer + m_size, m_buffer + m_capacity, value ); - if( ec == std::errc() ) [[likely]] + if( ec == std::errc() ) NFX_STRINGBUILDER_LIKELY { m_size = ptr - m_buffer; } @@ -294,12 +294,12 @@ namespace nfx::string inline StringBuilder& StringBuilder::append( float value ) { - if( m_size + FLOAT_MAX_CHARS > m_capacity ) [[unlikely]] + if( m_size + FLOAT_MAX_CHARS > m_capacity ) NFX_STRINGBUILDER_UNLIKELY { ensureCapacity( m_size + FLOAT_MAX_CHARS ); } auto [ptr, ec] = std::to_chars( m_buffer + m_size, m_buffer + m_capacity, value ); - if( ec == std::errc() ) [[likely]] + if( ec == std::errc() ) NFX_STRINGBUILDER_LIKELY { m_size = ptr - m_buffer; } @@ -308,12 +308,12 @@ namespace nfx::string inline StringBuilder& StringBuilder::append( double value ) { - if( m_size + DOUBLE_MAX_CHARS > m_capacity ) [[unlikely]] + if( m_size + DOUBLE_MAX_CHARS > m_capacity ) NFX_STRINGBUILDER_UNLIKELY { ensureCapacity( m_size + DOUBLE_MAX_CHARS ); } auto [ptr, ec] = std::to_chars( m_buffer + m_size, m_buffer + m_capacity, value ); - if( ec == std::errc() ) [[likely]] + if( ec == std::errc() ) NFX_STRINGBUILDER_LIKELY { m_size = ptr - m_buffer; } @@ -329,7 +329,7 @@ namespace nfx::string const size_t len = str.size(); const size_t newSize = m_size + len + 1; // +1 for newline - if( newSize > m_capacity ) [[unlikely]] + if( newSize > m_capacity ) NFX_STRINGBUILDER_UNLIKELY { ensureCapacity( newSize ); } @@ -354,7 +354,7 @@ namespace nfx::string inline StringBuilder& StringBuilder::appendLine( const char* str ) { - if( str ) [[likely]] + if( str ) NFX_STRINGBUILDER_LIKELY { return appendLine( std::string_view{ str, strlen( str ) } ); } @@ -383,7 +383,7 @@ namespace nfx::string inline StringBuilder& StringBuilder::prepend( char c ) { - if( m_size + 1 > m_capacity ) [[unlikely]] + if( m_size + 1 > m_capacity ) NFX_STRINGBUILDER_UNLIKELY { ensureCapacity( m_size + 1 ); } @@ -405,7 +405,7 @@ namespace nfx::string { char buffer[INT8_MAX_DIGITS]; auto [ptr, ec] = std::to_chars( buffer, buffer + INT8_MAX_DIGITS, static_cast( value ) ); - if( ec == std::errc() ) [[likely]] + if( ec == std::errc() ) NFX_STRINGBUILDER_LIKELY { return prepend( std::string_view{ buffer, static_cast( ptr - buffer ) } ); } @@ -416,7 +416,7 @@ namespace nfx::string { char buffer[UINT8_MAX_DIGITS]; auto [ptr, ec] = std::to_chars( buffer, buffer + UINT8_MAX_DIGITS, static_cast( value ) ); - if( ec == std::errc() ) [[likely]] + if( ec == std::errc() ) NFX_STRINGBUILDER_LIKELY { return prepend( std::string_view{ buffer, static_cast( ptr - buffer ) } ); } @@ -427,7 +427,7 @@ namespace nfx::string { char buffer[INT16_MAX_DIGITS]; auto [ptr, ec] = std::to_chars( buffer, buffer + INT16_MAX_DIGITS, value ); - if( ec == std::errc() ) [[likely]] + if( ec == std::errc() ) NFX_STRINGBUILDER_LIKELY { return prepend( std::string_view{ buffer, static_cast( ptr - buffer ) } ); } @@ -438,7 +438,7 @@ namespace nfx::string { char buffer[UINT16_MAX_DIGITS]; auto [ptr, ec] = std::to_chars( buffer, buffer + UINT16_MAX_DIGITS, value ); - if( ec == std::errc() ) [[likely]] + if( ec == std::errc() ) NFX_STRINGBUILDER_LIKELY { return prepend( std::string_view{ buffer, static_cast( ptr - buffer ) } ); } @@ -449,7 +449,7 @@ namespace nfx::string { char buffer[INT32_MAX_DIGITS]; auto [ptr, ec] = std::to_chars( buffer, buffer + INT32_MAX_DIGITS, value ); - if( ec == std::errc() ) [[likely]] + if( ec == std::errc() ) NFX_STRINGBUILDER_LIKELY { return prepend( std::string_view{ buffer, static_cast( ptr - buffer ) } ); } @@ -460,7 +460,7 @@ namespace nfx::string { char buffer[UINT32_MAX_DIGITS]; auto [ptr, ec] = std::to_chars( buffer, buffer + UINT32_MAX_DIGITS, value ); - if( ec == std::errc() ) [[likely]] + if( ec == std::errc() ) NFX_STRINGBUILDER_LIKELY { return prepend( std::string_view{ buffer, static_cast( ptr - buffer ) } ); } @@ -471,7 +471,7 @@ namespace nfx::string { char buffer[INT64_MAX_DIGITS]; auto [ptr, ec] = std::to_chars( buffer, buffer + INT64_MAX_DIGITS, value ); - if( ec == std::errc() ) [[likely]] + if( ec == std::errc() ) NFX_STRINGBUILDER_LIKELY { return prepend( std::string_view{ buffer, static_cast( ptr - buffer ) } ); } @@ -482,7 +482,7 @@ namespace nfx::string { char buffer[UINT64_MAX_DIGITS]; auto [ptr, ec] = std::to_chars( buffer, buffer + UINT64_MAX_DIGITS, value ); - if( ec == std::errc() ) [[likely]] + if( ec == std::errc() ) NFX_STRINGBUILDER_LIKELY { return prepend( std::string_view{ buffer, static_cast( ptr - buffer ) } ); } @@ -493,7 +493,7 @@ namespace nfx::string { char buffer[FLOAT_MAX_CHARS]; auto [ptr, ec] = std::to_chars( buffer, buffer + FLOAT_MAX_CHARS, value ); - if( ec == std::errc() ) [[likely]] + if( ec == std::errc() ) NFX_STRINGBUILDER_LIKELY { return prepend( std::string_view{ buffer, static_cast( ptr - buffer ) } ); } @@ -504,7 +504,7 @@ namespace nfx::string { char buffer[DOUBLE_MAX_CHARS]; auto [ptr, ec] = std::to_chars( buffer, buffer + DOUBLE_MAX_CHARS, value ); - if( ec == std::errc() ) [[likely]] + if( ec == std::errc() ) NFX_STRINGBUILDER_LIKELY { return prepend( std::string_view{ buffer, static_cast( ptr - buffer ) } ); } @@ -615,12 +615,14 @@ namespace nfx::string // Formatting operations //---------------------------------------------- +#if defined( NFX_STRINGBUILDER_HAS_STD_FORMAT ) template inline StringBuilder& StringBuilder::format( std::format_string fmt, Args&&... args ) { std::format_to( std::back_inserter( *this ), fmt, std::forward( args )... ); return *this; } +#endif // NFX_STRINGBUILDER_HAS_STD_FORMAT //---------------------------------------------- // Join operations @@ -742,6 +744,7 @@ namespace nfx::string } } // namespace nfx::string +#if defined( NFX_STRINGBUILDER_HAS_STD_FORMAT ) namespace std { //===================================================================== @@ -762,3 +765,4 @@ namespace std } }; } // namespace std +#endif // NFX_STRINGBUILDER_HAS_STD_FORMAT diff --git a/include/nfx/string/StringBuilder.h b/include/nfx/string/StringBuilder.h index 3fdb93a..416d002 100644 --- a/include/nfx/string/StringBuilder.h +++ b/include/nfx/string/StringBuilder.h @@ -71,11 +71,29 @@ #pragma once #include -#include #include #include #include +#if defined( __has_include ) +# if __has_include( ) +# include +# endif +#endif + +/** + * @def NFX_STRINGBUILDER_HAS_STD_FORMAT + * @brief Defined to 1 when C++20 std::format support is available + * @details std::format requires C++20. When building under C++17 (or with a + * standard library that lacks ), the format() method and the + * std::formatter specialization are disabled, while the rest of the + * StringBuilder API remains fully available. + */ +#if defined( __cpp_lib_format ) && __cpp_lib_format >= 201907L +# include +# define NFX_STRINGBUILDER_HAS_STD_FORMAT 1 +#endif + /** * @def NFX_STRINGBUILDER_FORCE_INLINE * @brief Cross-platform forced inline macro for critical hot-path methods @@ -88,6 +106,32 @@ # define NFX_STRINGBUILDER_FORCE_INLINE inline #endif +/** + * @def NFX_STRINGBUILDER_LIKELY + * @def NFX_STRINGBUILDER_UNLIKELY + * @brief Branch-prediction hint attributes (C++20 [[likely]]/[[unlikely]]) + * @details Expand to the C++20 attributes when supported and to nothing under + * C++17, keeping hot-path branch hints warning-free across standards. + * The language-version check is required because some compilers report + * __has_cpp_attribute(likely) as available in C++17 as an extension. + */ +#if defined( _MSVC_LANG ) +# define NFX_STRINGBUILDER_CPLUSPLUS _MSVC_LANG +#else +# define NFX_STRINGBUILDER_CPLUSPLUS __cplusplus +#endif + +#if NFX_STRINGBUILDER_CPLUSPLUS >= 202002L && defined( __has_cpp_attribute ) +# if __has_cpp_attribute( likely ) >= 201803L +# define NFX_STRINGBUILDER_LIKELY [[likely]] +# define NFX_STRINGBUILDER_UNLIKELY [[unlikely]] +# endif +#endif +#if !defined( NFX_STRINGBUILDER_LIKELY ) +# define NFX_STRINGBUILDER_LIKELY +# define NFX_STRINGBUILDER_UNLIKELY +#endif + namespace nfx::string { //===================================================================== @@ -676,6 +720,7 @@ namespace nfx::string // Formatting operations //---------------------------------------------- +#if defined( NFX_STRINGBUILDER_HAS_STD_FORMAT ) /** * @brief Format and append text using std::format * @tparam Args Types of the formatting arguments @@ -685,9 +730,11 @@ namespace nfx::string * @details Provides type-safe formatting directly into the builder without intermediate allocations. * Uses std::format_to with back_inserter for optimal performance. * Example: builder.format("User {} (ID: {}) logged in at {}", name, userId, timestamp); + * @note Requires C++20 std::format support (NFX_STRINGBUILDER_HAS_STD_FORMAT). */ template inline StringBuilder& format( std::format_string fmt, Args&&... args ); +#endif // NFX_STRINGBUILDER_HAS_STD_FORMAT //---------------------------------------------- // String conversion diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt index d6790f5..293fcc4 100644 --- a/samples/CMakeLists.txt +++ b/samples/CMakeLists.txt @@ -89,7 +89,7 @@ foreach(sample_source ${sample_sources}) set_target_properties(${sample_target_name} PROPERTIES - CXX_STANDARD 20 + CXX_STANDARD ${NFX_STRINGBUILDER_CXX_STANDARD} CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF POSITION_INDEPENDENT_CODE ON diff --git a/samples/Sample_StringBuilder.cpp b/samples/Sample_StringBuilder.cpp index 3eadb2d..14321dc 100644 --- a/samples/Sample_StringBuilder.cpp +++ b/samples/Sample_StringBuilder.cpp @@ -34,13 +34,16 @@ #include #include #include -#include #include #include #include #include #include +#if defined( NFX_STRINGBUILDER_HAS_STD_FORMAT ) +# include +#endif + //===================================================================== // Helper function: Calculate NMEA-0183 checksum //===================================================================== @@ -238,6 +241,7 @@ int main() std::cout << "8. std::format integration with StringBuilder\n"; std::cout << "-----------------------------------------------\n"; +#if defined( NFX_STRINGBUILDER_HAS_STD_FORMAT ) StringBuilder builder; builder.append( "Formatted content" ); @@ -250,6 +254,9 @@ int main() std::cout << std::format( "Debug: buffer size={}, content={}\n", builder.size(), builder ); std::cout << "Note: Zero-copy formatting via std::formatter specialization\n"; +#else + std::cout << "Note: std::format integration requires C++20 (disabled in this build)\n"; +#endif std::cout << "\n"; } @@ -260,6 +267,7 @@ int main() std::cout << "9. std::format integration with StringBuilder\n"; std::cout << "----------------------------------------------\n"; +#if defined( NFX_STRINGBUILDER_HAS_STD_FORMAT ) StringBuilder builder; builder.append( "Building..." ); @@ -269,6 +277,9 @@ int main() std::cout << std::format( "After append: {}\n", builder ); std::cout << "Note: Useful for debugging string construction process\n"; +#else + std::cout << "Note: std::format integration requires C++20 (disabled in this build)\n"; +#endif std::cout << "\n"; } @@ -317,6 +328,7 @@ int main() std::cout << "11. Type-safe formatting with format() method\n"; std::cout << "----------------------------------------------\n"; +#if defined( NFX_STRINGBUILDER_HAS_STD_FORMAT ) StringBuilder builder; std::string username = "Alice"; @@ -345,6 +357,9 @@ int main() std::cout << "Chaining: " << builder.toString() << "\n"; std::cout << "Note: format() uses std::format internally for zero-overhead type-safe formatting\n"; +#else + std::cout << "Note: std::format-based formatting requires C++20 (disabled in this build)\n"; +#endif std::cout << "\n"; } diff --git a/src/StringBuilder.cpp b/src/StringBuilder.cpp index 2085faa..8a86cbf 100644 --- a/src/StringBuilder.cpp +++ b/src/StringBuilder.cpp @@ -70,7 +70,7 @@ namespace nfx::string m_capacity{ other.m_capacity }, m_onHeap{ other.m_onHeap } { - if( m_onHeap ) [[likely]] + if( m_onHeap ) NFX_STRINGBUILDER_LIKELY { m_heapBuffer = std::make_unique( m_capacity ); m_buffer = m_heapBuffer.get(); @@ -89,7 +89,7 @@ namespace nfx::string m_capacity{ other.m_capacity }, m_onHeap{ other.m_onHeap } { - if( !m_onHeap ) [[likely]] + if( !m_onHeap ) NFX_STRINGBUILDER_LIKELY { // Fast move: just copy stack buffer content without clearing source std::memcpy( m_stackBuffer, other.m_stackBuffer, m_size ); @@ -108,7 +108,7 @@ namespace nfx::string StringBuilder& StringBuilder::operator=( const StringBuilder& other ) { - if( this != &other ) [[likely]] + if( this != &other ) NFX_STRINGBUILDER_LIKELY { if( other.m_onHeap ) { @@ -141,7 +141,7 @@ namespace nfx::string StringBuilder& StringBuilder::operator=( StringBuilder&& other ) noexcept { - if( this != &other ) [[likely]] + if( this != &other ) NFX_STRINGBUILDER_LIKELY { m_heapBuffer = std::move( other.m_heapBuffer ); m_size = other.m_size; @@ -172,7 +172,7 @@ namespace nfx::string StringBuilder& StringBuilder::prepend( std::string_view str ) { - if( str.empty() ) [[unlikely]] + if( str.empty() ) NFX_STRINGBUILDER_UNLIKELY { return *this; } @@ -180,7 +180,7 @@ namespace nfx::string const size_t len = str.size(); const size_t newSize = m_size + len; - if( newSize > m_capacity ) [[unlikely]] + if( newSize > m_capacity ) NFX_STRINGBUILDER_UNLIKELY { ensureCapacity( newSize ); } @@ -188,7 +188,7 @@ namespace nfx::string char* buf = m_buffer; // Shift existing content to the right using memmove (handles overlap) - if( m_size > 0 ) [[likely]] + if( m_size > 0 ) NFX_STRINGBUILDER_LIKELY { std::memmove( buf + len, buf, m_size ); } @@ -226,7 +226,7 @@ namespace nfx::string { // Transition from stack to heap m_heapBuffer = std::make_unique( newCapacity ); - if( m_size > 0 ) [[likely]] + if( m_size > 0 ) NFX_STRINGBUILDER_LIKELY { std::memcpy( m_heapBuffer.get(), m_stackBuffer, m_size ); } @@ -238,7 +238,7 @@ namespace nfx::string { // Expand existing heap buffer auto newBuffer = std::make_unique( newCapacity ); - if( m_size > 0 ) [[likely]] + if( m_size > 0 ) NFX_STRINGBUILDER_LIKELY { std::memcpy( newBuffer.get(), m_heapBuffer.get(), m_size ); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7d1c2f0..c97eda0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -144,7 +144,7 @@ foreach(test_source ${test_sources}) set_target_properties(${test_target_name} PROPERTIES - CXX_STANDARD 20 + CXX_STANDARD ${NFX_STRINGBUILDER_CXX_STANDARD} CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF POSITION_INDEPENDENT_CODE ON diff --git a/test/Tests_StringBuilder.cpp b/test/Tests_StringBuilder.cpp index 16dd17f..77b18da 100644 --- a/test/Tests_StringBuilder.cpp +++ b/test/Tests_StringBuilder.cpp @@ -334,7 +334,7 @@ namespace nfx::string::test TEST( StringBuilder, AppendInt32 ) { StringBuilder builder; - builder.append( static_cast( -2147483648 ) ); + builder.append( static_cast( -2147483647 - 1 ) ); EXPECT_EQ( builder.toString(), "-2147483648" ); builder.clear(); @@ -765,6 +765,7 @@ namespace nfx::string::test // Format operator //---------------------------------------------- +#if defined( NFX_STRINGBUILDER_HAS_STD_FORMAT ) TEST( StringBuilder, FormatBasic ) { StringBuilder builder; @@ -792,6 +793,7 @@ namespace nfx::string::test builder.append( "Start: " ).format( "{}", 100 ).append( " End" ); EXPECT_EQ( builder.toString(), "Start: 100 End" ); } +#endif // NFX_STRINGBUILDER_HAS_STD_FORMAT //---------------------------------------------- // String conversion @@ -944,8 +946,13 @@ namespace nfx::string::test TEST( StringBuilder, ComplexChaining ) { StringBuilder builder; +#if defined( NFX_STRINGBUILDER_HAS_STD_FORMAT ) ( ( builder.append( "Start" ).append( " middle " ).append( 42 ).format( " formatted: {}", "text" ) ) .append( " end" ) ); +#else + ( ( builder.append( "Start" ).append( " middle " ).append( 42 ).append( " formatted: text" ) ) + .append( " end" ) ); +#endif std::string result = builder.toString(); EXPECT_NE( result.find( "Start" ), std::string::npos );