Introduction
Why should one write a wrapper for a standard C++ string?
- Convenience: better usability through simplified interface.
- Extensibility: add, remove, and enhance string functionality.
Background
Motivating example
Assume that you are using std::string in your C++ program and you make a slight mistake, e.g. you erroneously think that the following code will insert "hello" at the beginning of str:
using namespace std;
void test1()
{
string str ("world!");
string hello ("Hello, ");
str.insert (str.begin(), hello); }
Instead the compiler (VC 7.1) generates the following output:
myTestFile.cpp
myTestFile.cpp(12) : error C2664: 'std::basic_string<_Elem,_Traits,_Ax>::_Myt
&std::basic_string<_Elem,_Traits,_Ax>::insert(std::basic_string<_Elem,_Traits
,_Ax>::size_type,const std::basic_string<_Elem,_Traits,_Ax>::_Myt &)' : canno
t convert parameter 2 from 'std::string' to 'std::basic_string<_Elem,_Traits,
_Ax>::size_type'
with
[
_Elem=char,
_Traits=std::char_traits<char>,
_Ax=std::allocator<char>
]
and
[
_Elem=char,
_Traits=std::char_traits<char>,
_Ax=std::allocator<char>
]
No user-defined-conversion operator available that can perform this
conversion, or the operator cannot be called
The explanation is that 'std::string' is a typedef of template< class charT, class traits = char_traits<charT>, class Allocator = allocator<charT> > class basic_string. The traits and the Allocator template parameters have default values. But you cannot 'typedef away' a type in C++. A typedef is just an alias for the original type. You see the full basic_string template several times in the error message. By the way, with g++ 3.4 the same error produces an error message of more than 40 lines (reformatted to approximately 80 characters per line). You must have seen similar template jumble while using the C++ standard library. The length of the error messages multiplies when std::string is used as a template argument in another template, e.g. in std::vector<std::string> or in std::list<std::string>.
In the following, I use 'std::string' as a shortcut for std::basic_string<...> template.
Getting rid of templates
The basic idea is to write a string class (a class, not a template) that uses std::string as implementation 'back-end' to avoid the template mess and to make the string class extensible for the user. For now the interface is same as that of the std::string interface. Enter lstring, the lightweight string class.
The implementation looks similar to the following code:
class lstring {
string_type imp;
public:
explicit lstring() {}
lstring (const charT* s) : imp (s) {}
lstring (const lstring& str) : imp (str.imp) {}
lstring& operator= (const lstring& str)
{ imp = str.imp; return *this; }
lstring& append (const lstring& str)
{ imp.append (str.imp); return *this; }
size_type length() const { return imp.length(); }
size_type find (const lstring& str, size_type pos = 0) const
{ return imp.find (str.imp, pos); }
int compare (const lstring& str) const
{ return imp.compare (str.imp); }
};
lstring contains no templates in the interface (almost)
- Wrappers are provided for
iterator and const_iterator; no namespace is used.
- The implementation mostly forwards calls to the underlying
std::string implementation 'back-end'.
I haven't explained what the string_type in the above code snippet means. string_type is the type of the underlying std::string. On Windows platform, string_type automatically adapts to Unicode (via _TCHAR) if _UNICODE is #defined.
class lstring
{
#if defined (_WIN32)
typedef _TCHAR charT;
#else
typedef char charT;
#endif
typedef std::basic_string<charT> string_type;
};
Much ado about nothing?
What have we gained by wrapping std::sting into a lightweight sting class? Let's first repeat the above example with lstring instead of std::string:
void test2()
{
lstring str ("world!");
lstring hello ("Hello, ");
str.insert (str.begin(), hello); }
The output:
myTestFile.cpp
myTestFile.cpp(12) : error C2664: 'lstring &lstring::insert(lstring::size_typ
e,const lstring &)' : cannot convert parameter 2 from 'lstring' to 'lstring::
size_type'
No user-defined-conversion operator available that can perform this
conversion, or the operator cannot be called
This is still not the error message one expects but, because no templates are involved (i.e. no code generation from templates is involved), it's a lot better than the original std::string error message given above.
Customizing lstring
A second advantage of lstring is that it is not a std::string any more. This means that you can freely change the code and adapt it to your needs.
Using the code
The usage of lstring is not much different from std string:
#include <iostream>
#include "lstring.h"
#if defined (_UNICODE)
# define std_cout std::wcout
#else
# define std_cout std::cout
#endif
using namespace std;
int main()
{
lstring hello (_T("Hello, world!"));
std_cout << hello << endl;
}
Links
Many interesting string articles and implementations can be found in the CodeProject MFC/C++ String section.
Conclusion
The main purpose of lstring is to make string handling easier with standard C++-like string class, implemented as a wrapper for std::string (also known as std::basic_string). lstring also serves as a basis for user defined enhancements and adaptations. A set of unit tests is also included. In the forthcoming article, I will describe the customization of a standard container.
History
- December 4th, 2005 - First release in CodeProject.