📜 ⬆️ ⬇️

Implementing ToString () in C ++

To output to the log (and not only for this, but this is what I myself encountered), you need to convert the value of the variable into a string.

In C ++, this is usually done by outputting to the stream (alternatively, using boost: lexical_cast <> - which in our case is almost the same).

For built-in types, this is not a problem, but what should we do if we need to display, say, std: vector? Alas, std: vector does not have a stream output operator.
')
As a result of solving this problem, I wrote code that I want to share with the community.


Main idea.


So first the main idea. The idea, in fact, is quite simple - to write a set of overloaded functions that will perform the conversion to a string.

The first question that confronts us is what kind of function prototype to use:

template < typename T >
void ToStream ( std :: wostream & strm, const T & val ) ;


or

template < typename T >
std :: wstring ToString ( const T & val ) ;


The second option seems more attractive - passing in a variable - returns a string.
But in terms of performance (we need performance, otherwise why we’ve gotten into C ++), the first option usually wins, because it does not create a temporary variable (of type std: wstring) for the return value.
Besides, a simple wrapper without problems gives us the second option:

template < typename T >
std :: wstring ToString ( const T & val )
{
std :: wostringstream strm ;
ToStream ( strm, val ) ;

return strm. str ( ) ;
}


The first problem is solved, now we are going to implement the ToStream () itself. The simplest option is the output through the output operator (sorry for the tautology).

template < typename T >
void ToStream ( std :: wostream & strm, const T & val )
{
strm << val ;
}


Stop! And what if the type has no output operator? Stumbled upon an initial problem. The solution is obvious - you need to allow this function only for types for which the operator of output to the stream is defined. In code, it looks like this:

// T
//
struct AnyType
{
template < class T >
AnyType ( T )
{
}
} ;

//
// , (operator<<)
template < class Char >
boost :: type_traits :: no_type operator << ( std :: basic_ostream < Char > & , AnyType ) ;

// T ( T operator<<)?
template < class T, class Char >
class IsOutStreamable
{
static std :: basic_ostream < Char > & GetStrm ( ) ;
static const T & GetT ( ) ;
static boost :: type_traits :: no_type Impl ( boost :: type_traits :: no_type ) ;
static boost :: type_traits :: yes_type Impl ( ... ) ;
public :
static const bool value = sizeof ( Impl ( GetStrm ( ) << GetT ( ) ) ) == sizeof ( boost :: type_traits :: yes_type ) ;
} ;

// === T
template < typename T >
typename boost :: enable_if_c < IsOutStreamable < T, wchar_t > :: value , void > :: type
ToStream ( std :: wostream & strm, const T & val )
{
strm << val ;
}


Well, the first stage is over.
What's next? Define the output for the type std: pair - we will output in the form "(T, U)“:

// === std::pair
template < typename T, typename U >
void ToStream ( std :: wostream & strm, const std :: pair < T, U > & val )
{
strm << L '(' ;
ToStream ( strm, val. first ) ;
strm << L ", " ;
ToStream ( strm, val. second ) ;
strm << L ')' ;
}


Did someone ask a question? Please repeat - it’s hard to hear at a distance of 10,000 kilometers ...
Why do we [recursively] call ToStream ()? Everything is very simple. The fact is that the types T and / or U in turn can be complex types, for example std: pair <int, std :: pair <int, int>>. In the case of a recursive call, we get the output in the form (0, (1, 2)), which we actually need.

The finest hour has come for standard containers (we print as "[3] (1, 2, 3)"):

// has_iterator ..
BOOST_MPL_HAS_XXX_TRAIT_DEF ( iterator ) ;
BOOST_MPL_HAS_XXX_TRAIT_DEF ( const_iterator ) ;
BOOST_MPL_HAS_XXX_TRAIT_DEF ( value_type ) ;

// " (STL container)"
// , ,
// iterator, const_iterator value_type, std::[w]string
template < typename T >
struct IsStdContainer
{
static const int value = boost :: mpl :: and_ <
has_iterator < T > ,
has_const_iterator < T > ,
has_value_type < T > ,
boost :: mpl :: not_ < boost :: is_same < T, std :: string > > ,
boost :: mpl :: not_ < boost :: is_same < T, std :: wstring > >
> :: value ;
} ;

// === STL ( , STL - . IsStdContainer )
template < typename T >
typename boost :: enable_if < IsStdContainer < T > , void > :: type
ToStream ( std :: wostream & strm, const T & val )
{
strm << L '[' << val. size ( ) << L "](" ;

if ( ! val. empty ( ) )
{
typename T :: const_iterator it = val. begin ( ) ;
ToStream ( strm, * it ++ ) ;
for ( ; it ! = val. end ( ) ; ++ it )
{
strm << L ", " ;
ToStream ( strm, * it ) ;
}
}

strm << L ')' ;
}


Now we define a transform for the bool type. Fortunately, this is simpler than previous functions. Only one small note - in my code in the hider (.h) only the function description, and the definition is made in the .cpp file. The reason is simple - if the leader is included in several .cpp files, the function is determined in several translation units, which is bad and linkovshik will inform us about it (gloating about its superiority). For template functions this does not happen. Solely for simplicity, I moved the definition of the function to the leader (which should not be done for working projects for the reason described above).

// === bool
void ToStream ( std :: wostream & strm, const bool & val )
{
strm << ( val ? L "true" : L "false" ) ;
}


Here, in brief, and all the basic functions. True, in my implementation there is also:

// === std::string
void ToStream ( std :: wostream & strm, const std :: string & val ) ;

// === char*
void ToStream ( std :: wostream & strm, char * val ) ;

// === const char*
void ToStream ( std :: wostream & strm, const char * val ) ;

// === const char
void ToStream ( std :: wostream & strm, const char val ) ;


For what? For output to the “wide” stream (std: wostream) of “narrow” strings / characters (char, std: string). The fact is that in my projects I deal with strings in the UTF8 format. Accordingly, I store such strings in std: string. In the ToStream functions (std: wostream & strm, const std: string & val), I convert the string from UFT8 to std: wstring and output it. I do not provide the function code, as it will complicate it, but it will not bring anything new in principle.

Now examples of use.
To begin with a couple of auxiliary macros (no need to throw stones at me! Sometimes macros can make life much easier).
First macro:

#define _VAR(var) L ## #var << L"<" << ToString(var) << L"> "

allows us to write code:

int i = 0 ;
int n = 10 ;
std :: cout << _VAR ( i ) << _VAR ( n ) ;


and get in the output:

i < 0 > n < 10 >

why not "i = 0 n = 10"? The reason is felt when displaying lines:

std :: string s1 = "" ;
std :: string s2 = " " ;
std :: cout << _VAR ( s1 ) << _VAR ( s2 ) ;


output (if the code highlighting does not fail, the difference will be obvious):

s1 <> s2 < >

The second macro for tests - if the condition is not met, then throws an exception:

#define CHECK(expr) \
if ( ! ( expr ) ) \
{ \
throw #expr; \
} \
else \
( ( void ) 0 )


Now actually examples:

Example 1. Output std: vector.


The simplest of examples.

std :: vector < int > v = boost :: assign :: list_of ( 0 ) ( 1 ) ( 2 ) ( 3 ) ;
CHECK ( ToString ( v ) == L "[4](0, 1, 2, 3)" ) ;
std :: wcout << _VAR ( v ) << std :: endl ;


Example 2. Displaying std: map.


This example is a bit more interesting because it uses 2 functions - for the container and for the std: pair (I remind you that map stores pairs in itself) - this is what we wrote about the output of pairs.

std :: map < int , int > m = boost :: assign :: map_list_of ( 0 , 1 ) ( 2 , 3 ) ( 4 , 5 ) ;
CHECK ( ToString ( m ) == L "[3]((0, 1), (2, 3), (4, 5))" ) ;
std :: wcout << _VAR ( m ) << std :: endl ;

< h4 > 3. std :: map . < / h4 >
. .

< code class = "cpp" >
std :: map < std :: wstring , std :: vector < int > > msv = boost :: assign :: list_of < std :: pair < std :: wstring , std :: vector < int > > >
( L "zero" , boost :: assign :: list_of ( 0 ) )
( L "one" , boost :: assign :: list_of ( 1 ) ( 2 ) )
( L "two" , boost :: assign :: list_of ( 2 ) ( 3 ) ( 4 ) )
;

CHECK ( ToString ( msv ) == L "[3]((one, [2](1, 2)), (two, [3](2, 3, 4)), (zero, [1](0)))" ) ;
std :: wcout << _VAR ( msv ) << std :: endl ;


I hope that no one is surprised that the output is different from what is written in the initialization. If it still surprises someone, then I advise you to remember what a std: map is and how data is stored there.

Example 4. Output custom types.


First the code.

enum RO4_ReplyType /// Reply type
{
RO4_RT_Mobile, ///< Replies go to mobile phone
RO4_RT_Email, ///< Replies go to email address
RO4_RT_MobileAndEmail ///< Replies go to mobile phone and to email address
} ;

RO4_ReplyType rt = RO4_RT_Email ;
CHECK ( RO4 :: Manip :: ToString ( rt ) == L "1" ) ;


Nothing interesting, the enum is cast to an integer type and its value is displayed. I would not give this banal example if it were not for the possibility of expanding my solution. Add the following code (again, the macro! Yes, I know, but it's easier for me):

/// Output operator for RO4_ReplyType
void ToStream ( std :: wostream & strm, const RO4_ReplyType & val )
{
#define STR(name) case name: strm << L## #name; break
switch ( val )
{
STR ( RO4_RT_Mobile ) ;
STR ( RO4_RT_Email ) ;
STR ( RO4_RT_MobileAndEmail ) ;

default :
strm << L "Unknown value of RO4_ReplyType<" << static_cast < int > ( val ) << L ">" ;
}
#undef STR
}


and lo and behold! The conclusion turns into:

RO4_ReplyType rt = RO4_RT_Email ;
CHECK ( RO4 :: Manip :: ToString ( rt ) == L "RO4_RT_Email" ) ;


Those. This example shows how to expand the possibilities of applying my solution.

Afterword.


PS: In a real implementation, everything is wrapped in namespaces. Full listings (with more pleasant syntax highlighting) here:

RO4_ToString.h
main.cpp

Source: https://habr.com/ru/post/81845/


All Articles