Using toString on Custom Types in C++

“Give me a string representation of this object.”

This is a fairly ubiquitous sentence in programming, that many languages express in one brief statement: Java has .toString() , Python has str and Haskell has show, to cite just a few.

My goal here is to propose a concise way to also express this in C++.

Note: after I wrote this post I realized that the same topic had been treated on nyorain’s blog, and in a very good manner. I still decided to go ahead and publish this post because the focus is slightly different:

the implementation is in C++14 (not C++17),

it illustrates the rules of Expressive Template Metaprogramming.

Indeed C++, at least to my knowledge, doesn’t have a native equivalent. Granted, there are a lot of powerful ways to build strings in C++, involving streams in particular. But there isn’t a convention for a small, concise expression, such as the ones in the other languages.

Converting one object into a string

Well, to be accurate, this is not entirely true. C++11 introduces the to_string overloads, but only for native numeric types (int, long, double, float and their unsigned counterparts).

But for a lot of types, stringification abilities are implemented by overloading operator<< to send data over to a stream:

std::ostream& operator<<(std::ostream& os, MyType const& myObject); 1 std :: ostream & operator << ( std :: ostream & os , MyType const & myObject ) ;

And to output an object into a stream, we have to use the following type of code:

MyType myObject = ... // myObject is initialized std::ostringstream myObjectStream; // a stream is built myObjectStream << myObject; // the stream is filled std::string mySerializedObject = myObjectStream.str(); // we extract the contents of the stream 1 2 3 4 5 6 MyType myObject = . . . // myObject is initialized std :: ostringstream myObjectStream ; // a stream is built myObjectStream << myObject ; // the stream is filled std :: string mySerializedObject = myObjectStream . str ( ) ; // we extract the contents of the stream

Even if this opens the possibility to elaborate string formatting and multiple objects going into the same string, this is quite a mouthful in our case to just express “Give me a string representation of this object.”

Now, nothing prevents the implementer of MyType to provide a to_string function, like the standard does for numeric types. But I find that it is a lot rarer, because types rather use the streams mechanism (which is a good thing for the power that it brings).

So to summarize, there are several ways to dump an object into a string in C++, and some are more complex (but powerful) than others.

Unifying the syntax

For this reason, I think we need a unified concise syntax for this job. I see the following advantages:

it would bring consistency across types,

it would “keep simple things simple”, by leaving the powerful tools like streams to more complex tasks (involving several objects or formatting),

well, nearly every other language does it. It’s not that we need to copy others languages, but in my opinion not having a tool for this simple task doesn’t help with the image of C++ being a complex language.

Now, there is existing code, implementing custom to_string methods, stream operations, and there is also the standard std::to_string for numerical types.

For this reason, let’s create a function compatible with all this, and that takes the best option available in each context. We’d have to agree on what’s “best” as on order, but for a start I propose the following, for a given type T:

1- if std::to_string is available for T then use it,

2- otherwise, if to_string on T exists in the same namespace as T then use it,

3- otherwise, if T can be streamed into an ostringstream then do it and return the resulting stream.

Implementation

In fact, all the heavy lifting is already done by the is_detected function from the experimental TS and that we re-implemented in Expressive C++ Template Metaprogramming.

This function returns a boolean indicating whether or not a given expression is valid. We use it to detect if each one of the above 3 attempts is successful:

// 1- detecting if std::to_string is valid on T template<typename T> using std_to_string_expression = decltype(std::to_string(std::declval<T>())); template<typename T> constexpr bool has_std_to_string = is_detected<std_to_string_expression, T>; // 2- detecting if to_string is valid on T template<typename T> using to_string_expression = decltype(to_string(std::declval<T>())); template<typename T> constexpr bool has_to_string = is_detected<to_string_expression, T>; // 3- detecting if T can be sent to an ostringstream template<typename T> using ostringstream_expression = decltype(std::declval<std::ostringstream&>() << std::declval<T>()); template<typename T> constexpr bool has_ostringstream = is_detected<ostringstream_expression, T>; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // 1- detecting if std::to_string is valid on T template < typename T > using std_to_string_expression = decltype ( std :: to_string ( std :: declval < T > ( ) ) ) ; template < typename T > constexpr bool has_std_to_string = is_detected < std_to_string_expression , T > ; // 2- detecting if to_string is valid on T template < typename T > using to_string_expression = decltype ( to_string ( std :: declval < T > ( ) ) ) ; template < typename T > constexpr bool has_to_string = is_detected < to_string_expression , T > ; // 3- detecting if T can be sent to an ostringstream template < typename T > using ostringstream_expression = decltype ( std :: declval < std :: ostringstream & > ( ) << std :: declval < T > ( ) ) ; template < typename T > constexpr bool has_ostringstream = is_detected < ostringstream_expression , T > ;

Given this specification, the name of the unifying function cannot be to_string , because it would go into an infinite recursion when checking for option #2. So let’s call it toString (although if you have a better name for it you’re welcome to suggest it).

There must be several implementations for toString , depending on what is available on a type T, and only one implementation can exist for a given T. This is a job cut out for enable_if :

// 1- std::to_string is valid on T template<typename T, typename std::enable_if<has_std_to_string<T>, int>::type = 0> std::string toString(T const& t) { return std::to_string(t); } // 2- std::to_string is not valid on T, but to_string is template<typename T, typename std::enable_if<!has_std_to_string<T> && has_to_string<T>, int>::type = 0> std::string toString(T const& t) { return to_string(t); } // 3- neither std::string nor to_string work on T, let's stream it then template<typename T, typename std::enable_if<!has_std_to_string<T> && !has_to_string<T> && has_ostringstream<T>, int>::type = 0> std::string toString(T const& t) { std::ostringstream oss; oss << t; return oss.str(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 1- std::to_string is valid on T template < typename T , typename std :: enable_if < has_std_to_string < T > , int > :: type = 0 > std :: string toString ( T const & t ) { return std :: to_string ( t ) ; } // 2- std::to_string is not valid on T, but to_string is template < typename T , typename std :: enable_if < ! has_std_to_string < T > && has_to_string < T > , int > :: type = 0 > std :: string toString ( T const & t ) { return to_string ( t ) ; } // 3- neither std::string nor to_string work on T, let's stream it then template < typename T , typename std :: enable_if < ! has_std_to_string < T > && ! has_to_string < T > && has_ostringstream < T > , int > :: type = 0 > std :: string toString ( T const & t ) { std :: ostringstream oss ; oss << t ; return oss . str ( ) ; }

Let’s try this out on an int , a type A that has a to_string method in its namespace, and a type B that can be sent over to a stream:

namespace NA { class A {}; std::string to_string(A const&) { return "to_string(A)"; } } namespace NB { class B {}; std::ostream& operator<<(std::ostream& os, B const&) { os << "oss(B)"; return os; } } std::cout << toString(42) << '

'; std::cout << toString(NA::A()) << '

'; std::cout << toString(NB::B()) << '

'; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 namespace NA { class A { } ; std :: string to_string ( A const & ) { return "to_string(A)" ; } } namespace NB { class B { } ; std :: ostream & operator << ( std :: ostream & os , B const & ) { os << "oss(B)" ; return os ; } } std :: cout << toString ( 42 ) << '

' ; std :: cout << toString ( NA :: A ( ) ) << '

' ; std :: cout << toString ( NB :: B ( ) ) << '

' ;

And the above code outputs:

42 to_string(A) oss(B) 1 2 3 42 to_string ( A ) oss ( B )

There we go!

What’s your take on that?

One of the reasons that I’m blogging about this sort of topic is so we can discuss it.

I’d imagine that we can do much better, and I’d like to hear your thoughts about it. Both on the need for a unified syntax, and on the way to go about it. We can achieve so much more as a group! Let’s take advantage of it.

Oh and, whatever your amount of experience, you’re welcome to voice your opinions about this!

Share this post! Don't want to miss out ?