I can implement the underlying storage using std::string anyways, but than at least I can switch the implementation without hanging everything, plus I could add custom stuff like conversion routines, etc...
This also leads to madness. Remember separation of concerns. Your string needs to store a sequence of characters and that's it. Everything else should be in a different class/module. Even std::string is considered to be a train-wreck by many people in the same committee that created it due to all the searching and modification routines built into it.
If you write your own string, I'd recommend the thinnest and simplest wrapper around a self-managed char* as possible. Something like:
class string final {
public:
string();
string(std::nullptr_t);
string(string const& src);
string(string && src);
string(char const* begin, char const* end);
string(char const* begin, size_t length);
explicit string(const char* zstr);
~string();
string& operator=(string const& src);
string& operator=(string && src);
string& operator=(std::nullptr_t);
bool empty() const;
explicit operator bool() const;
size_t size() const;
char operator[](size_t index) const;
const char* begin() const;
const char* c_str() const;
const char* end() const;
}
And that's it. I'd highly recommend you use 8-bit characters, always. Never treat the NUL byte as special (except in the zstr constructor, which calls std::strlen to compute the length), but always keep your string NUL-terminated for the cases you have to interact with old C-style APIs via c_str. Don't make your strings mutable but rather use a separate StringBuilder or the like for cases you need to programatically generate a string. Any other manipulation function (make_lower, find_substr, etc.) can and should be in a separate utility class or be free functions; this both keeps your string class simpler and less buggy and it makes it easier to offer a single interface for all types of string via overloads and ADL (your string, std::string, std::wstring, char*, vector<char>, etc.).
Note that comparison operators are not defined. Make those separate (free) operators. This is necessary anyway for layering with comparing string with char* and other string types. Comparison with strings also benefits a lot from offering a number of functor comparators since strings can be compared so many different ways (character case, sorting of numeric ranges, etc.).
It's also handy to make a variant of the above called something like 'string_ref' or 'string_range' or 'string_view' that does not own its contents. This makes it easy to pass subsets of strings around to algorithms without making copies. It's basically the same as the above, only you don't need move operators (because copying is trivial), you don't need a destructor (because it too is trivial), and you can't include c_str (because arbitrary subsets of a string will not be NUL-terminated). string_view is so handy it's part of a TS for a future version of C++.
update: forgot operator[] and notes on comparison operators.