I know, there are a few implementations of immutable strings out there, but my focus seems to be a little different.
My goal was to have a type that provided value semantics, but didn't incur the cost of dynamic memory allocation when constructed from a string literal which is already guaranteed to exist during the whole program runtime.
After refactoring, I ended up with two classes:
- one
str_ref
class similar tostring_view
(maybe I can switch togsl::string_span
once I have a c++14 compiler) primarily used as a parameter for functions that don't intend to copy / take ownership of the string. - The actual
const_string
class which is derived from it but also stores the a dynamically allocated character array if necessary (via a shared pointer)
str_ref
#include <algorithm>
#include <iterator>
#include <string>
#include <ostream>
class str_ref {
public:
//type defs
using value_type = char;
using traits_type = std::char_traits<value_type>;
using size_type = size_t;
using difference_type = std::ptrdiff_t;
using reference = value_type&;
using const_reference = const value_type&;
using pointer = value_type*;
using const_pointer = const value_type*;
using iterator = pointer;
using const_iterator = const_pointer;
using reverse_iterator = std::reverse_iterator<iterator>;
using const_reverse_iterator = std::reverse_iterator<const_iterator>;
public:
/* #### CTORS #### */
constexpr str_ref() = default;
str_ref(const std::string& other) noexcept:
_start(other.data()),
_size(other.size())
{}
constexpr str_ref(const char* other, size_t size) noexcept :
_start(other),
_size(size)
{}
//NOTE: Use only for string literals!!!
template<size_t N>
constexpr str_ref(const char(&other)[N]) noexcept :
_start(other),
_size(N - 1)
{}
template<class T>
str_ref(const T * const& other) = delete;
/* #### Special member functions #### */
str_ref(const str_ref& other) = default;
str_ref& operator=(const str_ref& other) = default;
/* #### container functions #### */
constexpr const_reference first() const { return *begin(); }
constexpr const_reference last() const { return *(cend() - 1); }
constexpr const_iterator cbegin() const noexcept { return _start; }
constexpr const_iterator cend() const noexcept { return _start + _size; }
constexpr const_iterator begin() const noexcept { return cbegin(); }
constexpr const_iterator end() const noexcept { return cend(); }
const_reverse_iterator crbegin() const { return const_reverse_iterator(cend()); }
const_reverse_iterator crend() const { return const_reverse_iterator(cbegin()); }
const_reverse_iterator rbegin() const { return const_reverse_iterator(cend()); }
const_reverse_iterator rend() const { return const_reverse_iterator(cbegin()); }
constexpr size_type size() const noexcept { return _size; }
constexpr bool empty() const noexcept { return size() == 0; }
constexpr const_reference operator[](size_t idx) const {
return _start[idx];
}
/*#### string functions ####*/
std::string to_string() {
return std::string(begin(), end());
}
constexpr str_ref sub_string(size_t offset, size_t count) const {
return str_ref{ this->_start + offset, count };
}
int compare(const str_ref& other) const {
if ((begin() == other.begin()) && (size() == other.size())) {
return 0;
}
return std::lexicographical_compare(cbegin(), cend(), other.cbegin(), other.cend());
}
protected:
const char* _start = nullptr;
size_type _size = 0;
};
/* operator overloads */
bool operator==(const str_ref& l, const str_ref& r) { return l.compare(r) == 0; }
bool operator!=(const str_ref& l, const str_ref& r) { return !(l == r); }
bool operator< (const str_ref& l, const str_ref& r) { return l.compare(r) < 0; }
bool operator> (const str_ref& l, const str_ref& r) { return r<l; }
bool operator<=(const str_ref& l, const str_ref& r) { return !(l>r); }
bool operator>=(const str_ref& l, const str_ref& r) { return !(l<r); }
std::ostream& operator<<(std::ostream& out, const str_ref& string) {
out.write(&*(string.begin()), string.size());
return out;
}
const_string
#include <memory>
#include "StrRef.h"
class const_string;
namespace _impl_helper {
template<class ...ARGS>
const_string concat_impl(const ARGS&...args);
}
class const_string : public str_ref {
public:
/* #### CTORS #### */
const_string() = default;
const_string(const char* other, size_t size) { _copyFrom(other, size ); }
explicit const_string(const std::string& other) { _copyFrom(other.c_str(), other.size()); }
explicit const_string(const str_ref& other) { _copyFrom(other.cbegin(), other.size()); }
// don't accept c-strings
template<class T>
const_string(const T * const& other) = delete;
//NOTE: Use only for string literals (arrays with static storage duration)!!!
template<size_t N>
explicit constexpr const_string(const char(&other)[N]) noexcept :
str_ref(other, N - 1)
{}
/* #### Special member functions #### */
const_string(const const_string& other) = default;
const_string(const_string&& other) = default;
const_string& operator=(const const_string& other) = default;
const_string& operator=(const_string&& other) = default;
/* #### String functions #### */
const_string sub_string(size_t offset, size_t count) const {
const_string retval;
retval._start = this->_start + offset;
retval._size = count;
retval._data = this->_data;
return retval;
}
template<class ...ARGS>
friend const_string _impl_helper::concat_impl(const ARGS&...args);
private:
std::shared_ptr<char> _data = nullptr;
void _copyFrom(const char* other, size_t size) {
_data = std::shared_ptr<char>(new char[size], std::default_delete<char[]>());
std::copy_n(other, size, _data.get());
_size = size;
_start = _data.get();
}
};
namespace _impl_helper {
void addTo(char*& buffer, const str_ref& str) {
std::copy_n(str.begin(), str.size(), buffer);
buffer += str.size();
}
template<class ...ARGS>
const_string concat_impl(const ARGS& ...args) {
//determine total size
size_t newSize = 0;
int ignore[] = { (newSize += args.size(),0)... };
//create chonst_string object
const_string retval;
retval._data = std::shared_ptr<char>(new char[newSize], std::default_delete<char[]>());
retval._start = retval._data.get();
retval._size = newSize;
//place copy arguments to buffer
char * bufferStart = retval._data.get();
int ignore2[] = { (addTo(bufferStart,args),0)... };
return retval;
}
}
template<class ...ARGS>
const_string concat(ARGS&&...args) {
return _impl_helper::concat_impl(str_ref(args)...);
}
Aside from general advice of how to improve my class (I bet, there is a lot), I'd especially like to know if
- The creation of a new string can be made cheaper (currently it incurs two dynamic memory allocations - one for the string and one for the shared_ptr control block)
- You see a way to implement concat, that doesn't require the forward declaration of the
_impl_helper
namespace (I'd like to keep it stashed away at the end of the file, not in front of the actual class) - You think that it is a problem, that the internal representation of the string is not zero-terminated. In my current project, there are very few 3rd party functions that require zero terminated strings (and I try to avoid them myself where ever possible - hence this class), but what is your experience?
I also made the deliberate design choice, to not provide a virtual destructor due to the overhead it would incur and the fact that I don't see any use case, where I would want to destruct const_string via a pointer to str_ref
. Still this goes against best practices and might be suprising for other people using that code - would you accept such a code in your codebase?
I haven't finished documentation and the unit tests yet (I hope the code is readable enough), but here is some sample code to play around with:
#include <iostream>
#include "const_string.h"
using namespace std;
namespace {
void print_str_ref(str_ref s) {
std::cout << s << std::endl;
}
}
int main() {
//constexpr const char* tmp{ "Hello World" };
//const const_string ccs(tmp); //<-should produce compiler error
const_string cs0;
const_string cs1("Hello World");
const_string cs2("Hello World"s);
const char* t = "Hello World";
const_string cs3(t, std::strlen(t));
const_string cs4(cs1);
const_string cs5("tmp");
cs5 = cs1;
const_string cs6;
cs6 = cs2;
const_string cs71("Hello 12"s);
const_string cs72("World"s);
const_string cs7(concat(cs71.sub_string(0, 6), cs72));
std::string cppStr(cs7.to_string());
std::cout << cs1 << ":" << (cs1 == cs1) << std::endl;
std::cout << cs2 << ":" << (cs1 == cs2) << std::endl;
std::cout << cs3 << ":" << (cs1 == cs3) << std::endl;
std::cout << cs4 << ":" << (cs1 == cs4) << std::endl;
std::cout << cs5 << ":" << (cs1 == cs5) << std::endl;
std::cout << cs6 << ":" << (cs1 == cs6) << std::endl << std::endl;
std::cout << cppStr << ":" << (cppStr == "Hello World"s) << std::endl << std::endl;
print_str_ref("Hello World");
print_str_ref("Hello World"s);
print_str_ref(cs1);
print_str_ref({ t,std::strlen(t) });
std::cout << std::endl;
const str_ref tstr("ld");
print_str_ref(concat("Hel", "lo"s, const_string(" W"), str_ref("or"), tstr));
}
/*#### static / Constexpr stuff ####*/
constexpr str_ref str("Hello");
constexpr auto it = str.begin() + 4;
static_assert(*it == 'o', "");
template<class T>
struct CheckNothrowDefaults {
static_assert(std::is_nothrow_copy_assignable<T>::value, "");
static_assert(std::is_nothrow_move_assignable<T>::value, "");
static_assert(std::is_nothrow_copy_constructible<T>::value, "");
static_assert(std::is_nothrow_move_constructible<T>::value, "");
};
CheckNothrowDefaults<str_ref> _1{};
CheckNothrowDefaults<const_string> _2{};
Note: So far I can't use c++14 features, as the compiler for my embedded system is based on gcc4.8