A lightweight C++ header-only library for safe and efficient interoperation with C APIs that either accept or return C-style strings.
C++ codebases often find themselves using C APIs for certain functionalities. These APIs can sometimes accept a null-terminated const char* string (non-owning) as an input argument, or return a heap-allocated char* string (owning) as their output, or both. The current C++ standard defines std::string_view and std::string classes to represent non-owning and owning string types respectively. However, these do not always safely or efficiently interoperate with C APIs:
- A
std::string_viewobject cannot always be safely passed to a C API accepting a null-terminatedconst char*string since the former does not guarantee null-termination.
Note
std::string type does provide the null-termination guarantee, but it's an owning type and can lead to unnecessary allocations if not used carefully.
- On the other hand, a
std::stringobject cannot be constructed from a heap-allocatedchar*string returned by a C API without performing a deep copy. This double allocation can sometimes be undesirable, especially when the intention is only to perform string operations on the returned value.
Note
std::string_view type can be constructed from the returned char* string to facilitate string operations, but it's a non-owning type and can lead to a memory leak if not used carefully.
Therefore, there is a need for string types that can bridge the gap between standard C++ strings and raw C-style strings for better interoperation.
To address the challenges outlined above, this repository introduces two types: tuli::cstring_view and tuli::cstring. These types are designed to safely and efficiently bridge the gap between C++ and C string semantics, making interoperation seamless and less error-prone.
A lightweight, non-owning view over a null-terminated C-style string. Unlike std::string_view, tuli::cstring_view guarantees null-termination, making it safe to pass directly to C APIs expecting a null-terminated const char* string. It can be constructed from a const char*, std::string, or a null-terminated std::string_view object, and provides utility methods to get the length, check for emptiness, and access the underlying C-style string.
Key features:
- Guarantees null-termination for safe C API interop
- Non-owning, zero-overhead abstraction
- Provides
length(),is_empty(), andc_str()utility methods - Implicit conversion to
std::string_viewfor C++ compatibility - Stream output support via
operator<< - User-defined literal operator (e.g.
"example"_csv) for convenient construction
An owning, RAII-enabled wrapper for heap-allocated C-style strings. This type takes ownership of the char* string returned by a C API (including nullptr if the API signals failure that way), manages its lifetime, and ensures proper deallocation upon scope exit. It avoids unnecessary deep copies and memory leaks, while still allowing string operations via implicit conversion to std::string_view.
Key features:
- Owns and manages the lifetime of a heap-allocated
char*string - Move-only semantics to prevent accidental copies
- Provides
length(),is_empty(),is_null(), andc_str()utility methods - Implicit conversion to
std::string_viewfor C++ compatibility - Stream output support via
operator<< - Ensures memory is freed automatically, preventing leaks
Note
If unspecified, tuli::default_delete is used as the default Deleter. Internally, this calls std::free() on the allocated char* string upon scope exit, making it suitable for memory allocated using standard C allocators such as malloc, calloc, or realloc.
Tip
Some C APIs also provide custom deallocator methods to free previously allocated char* strings. To enable proper cleanup in such cases, a custom deleter type can be passed as the template parameter when using tuli::cstring. The requirements for such a deleter are the same as those imposed by the std::unique_ptr type.
- A C++ compiler with C++17 support or higher
- No external dependencies
Tested on the following platforms and compilers:
| Platform | Compiler | C++ Standards | Compiler Flags |
|---|---|---|---|
| Linux | GCC, Clang | C++17, C++20, C++23 | -Wall -Wextra -Werror -Wpedantic |
| macOS | Apple Clang | C++17, C++20, C++23 | -Wall -Wextra -Werror -Wpedantic |
| Windows | MSVC | C++17, C++20, C++23 | /W4 /WX /permissive- |
Just copy the files present in include/ directory to your repository's include directory and you are ready to go!
No separate compiling/linking required.
This project uses GoogleTest to write and run tests. All test cases are located in the tests/ directory. To run the test cases, follow the steps given below:
Important
Prerequisites:
- CMake version 3.14 or higher
- A C++ compiler with C++17 support
- Clone the repository
git clone https://github.com/ArnavT005/cstring-lite.git
cd cstring-lite
- Build project with
CMAKE_BUILD_TESTSCMake option set toTRUE
cmake -B build -DCMAKE_BUILD_TESTS=TRUE -S .
cmake --build build
- Run the tests
You can run the tests using CTest:
cd build
ctest --test-dir tests/
Or run the test binary directly:
./build/tests/tests
Note
GoogleTest is automatically downloaded by CMake during the first build, so no manual setup is required.
Sample examples are present in the examples/ directory. The following snippets illustrate the usage scenarios:
- Using
tuli::cstring_viewfor making safe and efficient C API wrappers
#include <cstdlib>
#include <string>
#include <string_view>
#include <tuli/cstring_view.hpp>
extern int some_c_api(const char* str); // returns -ve value on failure
[[nodiscard]] int some_c_api_wrapper(tuli::cstring_view csv) noexcept {
return some_c_api(csv.c_str());
}
using std::operator""s;
using std::operator""sv;
int main() {
const auto s{"C-style string"};
const auto sv{"std::string_view (null-terminated)"sv};
const auto str{"std::string"s};
if (some_c_api_wrapper(s) < 0 ||
some_c_api_wrapper({tuli::null_terminated, sv}) < 0 ||
some_c_api_wrapper(str) < 0 ) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}- Using
tuli::cstringto safely and efficiently wrap heap-allocatedchar*string and use it as a string type
#include <string_view>
#include <tuli/cstring.hpp>
#include <tuli/cstring_view.hpp>
extern char* some_c_api(const char* str); // returns malloced duplicated 'str'
[[nodiscard]] tuli::cstring<> some_c_api_wrapper(tuli::cstring_view csv) noexcept {
return {tuli::owned, some_c_api(csv.c_str())};
}
[[nodiscard]] bool some_cpp_api(std::string_view sv) noexcept {
const auto result_1{sv == "std::string_view"};
const auto result_2{sv.substr(0, 3) == "std"};
const auto result_3{sv.find("str") != std::string_view::npos};
return (result_1 && result_2 && result_3);
}
using std::operator""sv;
int main() {
const auto sv{"std::string_view"sv};
const auto cstr{some_c_api_wrapper({tuli::null_terminated, sv})};
return some_cpp_api(cstr) ? EXIT_SUCCESS : EXIT_FAILURE;
}To build and run examples, follow the steps given below: (for prerequisites and cloning instructions, see Running Tests section)
- Build project with
CMAKE_BUILD_EXAMPLESCMake option set toTRUE
cmake -B build -DCMAKE_BUILD_EXAMPLES=TRUE -S .
cmake --build build
- Run the examples
./build/examples/examples
Expected output:
C-style string says hello!
std::string_view (null-terminated) says hello!
std::string says hello!
Examples ran successfully.
A number of proposals have been made to introduce a null-terminated cstring_view type in the C++ standard library. These include the following:
- P1402R0: std::cstring_view - a C compatible std::string_view adapter (rejected)
- P3655R3: std::cstring_view (proposed for inclusion in C++29)
Implementations for the same can also be found on GitHub here and here. Though these solve the null-termination issue in similar ways, this library takes a more minimal approach — most string operations are delegated to std::string_view via implicit conversion, keeping cstring_view focused on its primary role at the C/C++ boundary rather than serving as a general-purpose string type.
tuli::cstring<Deleter> is essentially a one-to-one wrapper over std::unique_ptr<char, Deleter>, so a std::unique_ptr can, in theory, replace the usage of tuli::cstring. However, tuli::cstring offers some semantic advantages over std::unique_ptr:
tuli::cstringmakes the programmer's intention clear to the reader and is more expressive.std::unique_ptrimplementation can possibly lead to errors if not used carefully.tuli::cstringprovides implicit conversion tostd::string_view, which makes performing string operations on the returnedchar*string smoother and less error-prone.