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.