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
- Implicit conversion to
std::string_viewfor C++ compatibility - 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, 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 - 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.
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.24 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
TULI_ENV_BUILD_TESTSenvironment variable set toTRUE
cmake -B build -DTULI_ENV_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/cstring-lite-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>
#include <tuli/cstring_view>
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
TULI_ENV_BUILD_EXAMPLESenvironment variable set toTRUE
cmake -B build -DTULI_ENV_BUILD_EXAMPLES=TRUE -S .
cmake --build build
- Run the examples
./build/examples/cstring-lite-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, the definition of cstring_view class in above works is quite extensive given that most of the functionalities can be simply achieved via an implicit conversion to std::string_view. This seems rather unnecessary as such a type should ideally not substitute the use of std::string_view in C++ code, but only used when necessary (at the C/C++ boundary).
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.