C++ convert simple values to string

2019-02-05 17:51发布

问题:

Right now I use the following piece of code to dummily convert basic types (int, long, char[], this kind of stuff) to std::string for further processing:

template<class T>
constexpr std::string stringify(const T& t)
{
    std::stringstream ss;
    ss << t;
    return ss.str();
}

however I don't like the fact that it depends on std::stringstream. I tried using std::to_string (from C++11's repertoire) however it chokes on char[] variables.

Is there a simple way offering an elegant solution for this problem?

回答1:

As far as I know the only way of doing this is by specialising the template by the parameter type with SFINAE.

You need to include the type_traits.

So instead of your code use something like this:

template<class T>
 typename std::enable_if<std::is_fundamental<T>::value, std::string>::type stringify(const T& t)
  {
    return std::to_string(t);
  }

template<class T>
  typename std::enable_if<!std::is_fundamental<T>::value, std::string>::type  stringify(const T& t)
  {
    return std::string(t);
  }

this test works for me:

int main()
{
  std::cout << stringify(3.0f);
  std::cout << stringify("Asdf");
}

Important note: the char arrays passed to this function need to be null terminated!

As noted in the comments by yakk you can get rid of the null termination with:

template<size_t N> std::string stringify( char(const& s)[N] ) { 
    if (N && !s[N-1]) return {s, s+N-1};
    else return {s, s+N}; 
}


回答2:

Is there a simple way offering an elegant solution for this problem?

Since nobody proposed it, consider using boost::lexical_cast.

This integrates seamlessly with anything that implements std::ostream<< operator and can be extended for custom types.



回答3:

I'd recommend using enable_if_t and if you're going to take in any single character variables you specialize those:

template<typename T>
enable_if_t<is_arithmetic<T>::value, string> stringify(T t){
    return to_string(t);
}

template<typename T>
enable_if_t<!is_arithmetic<T>::value, string> stringify(T t){
    return static_cast<ostringstream&>(ostringstream() << t).str();
}

template<>
string stringify<char>(char t){
    return string(1, t);
}

Here I'm just specializing char. If you need to specialize wchar, char16, or char32 you'll need to do that as well.

Anyway for non-arithmetic types these overloads will default to using ostringstream which is good cause if you've overloaded the extraction operator for one of your classes this will handle it.

For arithmetic types this will use to_string, with the exception of char and anything else you overload, and those can directly create a string.

Edit:

Dyp suggested using whether to_string accepts an argument of T::type as my enable_if_t condition.

The simplest solution is only available to you if you have access to is_detected in #include <experimental/type_traits>. If you do just define:

template<typename T>
using to_string_t = decltype(to_string(declval<T>()));

Then you can set your code up as:

template<typename T>
decltype(to_string(T{})) stringify(T t){
    return to_string(t);
}

template<typename T>
enable_if_t<!experimental::is_detected<to_string_t, T>::value, string> (T t){
    return static_cast<ostringstream&>(ostringstream() << t).str();
}

template<>
string stringify<char>(char t){
    return string(1, t);
}

I asked this question to figure out how to use to_string as my condition. If you don't have access to is_detected I'd highly recommend reading through some of the answers cause they are phenomenal: Metaprograming: Failure of Function Definition Defines a Separate Function



回答4:

The simplest solution is to overload for the types you want:

using std::to_string;

template<size_t Size>
std::string to_string(const char (&arr)[Size])
{
    return std::string(arr, Size - 1);
}

since to_string isn't a template you can't specialize it, but fortunately this is easier.

The code assumes the array is null terminated, but is still safe if it is not.

You may also want to put the using line inside the functions that call to_string if you have strong feelings about where using belongs.

This also has the benefit that if you pass it a non-null-terminated string somehow, it does not have UB as the one argument std::string constructor does.



回答5:

Although the the question is not of a gimme the code kind, since I already have a solution implemented I thought of sharing it:

template <class... Tail>
inline auto buildString(std::string const &head, Tail const &... tail)
    -> std::string;

template <class... Tail>
inline auto buildString(char const *head, Tail const &... tail) -> std::string;

template <class... Tail>
inline auto buildString(char *head, Tail const &... tail) -> std::string;

template <class Head, class... Tail>
inline auto buildString(Head const &head, Tail const &... tail) -> std::string;

inline auto buildString() -> std::string { return {}; }

template <class... Tail>
inline auto buildString(std::string const &head, Tail const &... tail)
    -> std::string {
  return head + buildString(tail...);
}
template <class... Tail>
inline auto buildString(char const *head, Tail const &... tail) -> std::string {
  return std::string{head} + buildString(tail...);
}
template <class... Tail>
inline auto buildString(char *head, Tail const &... tail) -> std::string {
  return std::string{head} + buildString(tail...);
}
template <class Head, class... Tail>
inline auto buildString(Head const &head, Tail const &... tail) -> std::string {
  return std::to_string(head) + buildString(tail...);
}

Usage:

auto gimmeTheString(std::string const &str) -> void {
  cout << str << endl;
}

int main() {

  std::string cpp_string{"This c++ string"};
  char const c_string[] = "this c string";

  gimmeTheString(buildString("I have some strings: ", cpp_string, " and ",
                             c_string, " and some number ", 24));
  return 0;
}


回答6:

I believe, the most elegant solution is:

#include <string>

template <typename T>
typename std::enable_if<std::is_constructible<std::string, T>::value, std::string>::type
stringify(T&& value) {
    return std::string(std::forward<T>(value)); // take advantage of perfect forwarding
}

template <typename T>
typename std::enable_if<!std::is_constructible<std::string, T>::value, std::string>::type
stringify(T&& value) {
    using std::to_string; // take advantage of ADL (argument-dependent lookup)
    return to_string(std::forward<T>(value)); // take advantage of perfect forwarding
}

Here, if we can construct std::string using T (we check it with help of std::is_constructible<std::string, T>), then we do it, otherwise we use to_string.

Of course, in C++14 you can replace typename std::enable_if<...>::type with much shorter std::enable_if_t<...>. An example is in the shorter version of the code, right below.

The following is a shorter version, but it's a bit less efficient, because it needs one extra move of std::string (but if we do just a copy instead, it's even less efficient):

#include <string>

std::string stringify(std::string s) { // use implicit conversion to std::string
    return std::move(s); // take advantage of move semantics
}

template <typename T>
std::enable_if_t<!std::is_convertible<T, std::string>::value, std::string>
stringify(T&& value) {
    using std::to_string; // take advantage of ADL (argument-dependent lookup)
    return to_string(std::forward<T>(value)); // take advantage of perfect forwarding
}

This version uses implicit conversion to std::string then possible, and uses to_string otherwise. Notice the usage of std::move to take advantage of C++11 move semantics.

Here is why my solution is better than the currently most voted solution by @cerkiewny:

  • It have much wider applicability, because, thanks to ADL, it is also defined for any type for which conversion using function to_string is defined (not only std:: version of it), see the example usage below. Whereas the solution by @cerkiewny only works for the fundamental types and for the types from which std::string is constructible.

    Of course, in his case it is possible to add extra overloads of stringify for other types, but it is a much less solid solution if compared to adding new ADL versions of to_string. And chances are height, that ADL-compatible to_string is already defined in a third party library for a type we want to use. In this case, with my code you don't need to write any additional code at all to make stringify work.

  • It is more efficient, because it takes advantage of C++11 perfect forwarding (by using universal references (T&&) and std::forward).

Example usage:

#include <string>

namespace Geom {
    class Point {
    public:
        Point(int x, int y) : x(x), y(y) {}

        // This function is ADL-compatible and not only 'stringify' can benefit from it.
        friend std::string to_string(const Point& p) {
            return '(' + std::to_string(p.x) + ", " + std::to_string(p.y) + ')';
        }
    private:
        int x;
        int y;
    };
}

#include <iostream>
#include "stringify.h" // inclusion of the code located at the top of this answer

int main() {
    double d = 1.2;
    std::cout << stringify(d) << std::endl; // outputs "1.200000"

    char s[] = "Hello, World!";
    std::cout << stringify(s) << std::endl; // outputs "Hello, World!"

    Geom::Point p(1, 2);
    std::cout << stringify(p) << std::endl; // outputs "(1, 2)"
}

Alternative, but not recommended approach

I also considered just overloading to_string:

template <typename T>
typename std::enable_if<std::is_constructible<std::string, T>::value, std::string>::type
to_string(T&& value) {
    return std::string(std::forward<T>(value)); // take advantage of perfect forwarding
}

And a shorter version using implicit conversion to std::string:

std::string to_string(std::string s) { // use implicit conversion to std::string
    return std::move(s); // take advantage of move semantics
}

But these have serious limitations: we need to remember to write to_string instead of std::to_string everywhere where we want to use it; also it is incompatible with the most common ADL usage pattern:

int main() {
    std::string a = std::to_string("Hello World!"); // error

    using std::to_string; // ADL
    std::string b = to_string("Hello World!"); // error
}

And it's most probable, there are other problems connected with this approach.