C++将简单值转换为字符串

22

目前,我使用以下代码将基本类型(如intlongchar[]等)愚蠢地转换为std::string并进行进一步处理:

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

然而,我不喜欢它依赖于std::stringstream。我尝试使用C++11中的std::to_string函数,但它对char[]变量无法处理。

是否有一种简单而优雅的解决方案来解决这个问题?


11
遇到了类似的问题,最终在处理字面量和char[]时进行了特殊化模板...希望有人知道更简单的解决方案。 - cerkiewny
@cerkiewny 你应该把那个作为一个答案发表出来。 - vsoftco
你到底是因为什么不喜欢std::stringstream依赖?因为在std::to_string出现之前,我就用过SSTR()宏,一直很喜欢它的多个<<链接的能力,但不能真正将其作为答案发布,因为你说“不使用stringstream”... - DevSolar
1
相关。其中提到了以下方法:stringstream、to_string、boost::spirit::karma、boost::lexical_cast。 - Nikos Athanasiou
6个回答

10
据我所知,唯一实现此功能的方法是通过使用SFINAE将模板针对参数类型进行特化。您需要包含type_traits库。因此,您可以使用类似以下代码的方式而不是您的代码:
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);
  }

这个测试对我有效:
int main()
{
  std::cout << stringify(3.0f);
  std::cout << stringify("Asdf");
}

重要提示:传递给此函数的字符数组需要以空字符结尾!

正如yakk在评论中指出的那样,您可以通过以下方式去除空字符结尾:

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
@black 我的答案可以解决这个问题。你只需要稍微调整一下 enable_if 并添加 ostringstream 即可。 - Jonathan Mee
1
测试 std::to_string(t) 是否符合 SFINAE 条件可能是更好的检查方式。例如,template<class T> auto stringify(T&& t) -> decltype(std::to_string(std::forward<T>(t))) { return std::to_string(std::forward<T>(t)); } - dyp
1
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}; } 可以消除对空终止要求。 - Yakk - Adam Nevraumont
1
如жһњдҢ ж­ӘењЁдҢүз”Ёc++14пәЊдҢ еЏҮд»ӨдҢүз”Ёenable_if_t<...>д»Әж›үtemplate enable_it<...>::typeгЂ‚ - Jonathan Mee
2
在这个答案中,“constexpr”是无意义的,因为“std::string”不是字面类型。此外还有更严重的缺陷,请参见我的答案了解详情。 - dened
显示剩余3条评论

9

有没有一种简单的方式提供一个优雅的解决方案?

由于没有人提出,可以考虑使用 boost::lexical_cast

这个方法与任何实现了std::ostream<<运算符的东西无缝集成,并且可以扩展到自定义类型。


我其实想过这个问题,但由于他不满意 STL 对字符串流的依赖性,我认为 boost::lexical_cast 也不是解决方法...但肯定是不错的替代选择。 - cerkiewny
3
我以前也发现过这一点:人们倾向于认为对一个对象具有依赖性会使代码变得笨重/资源占用高/缓慢/难看。 - utnapistim
1
boost::lexical_cast,在我的经验中,速度非常慢,以至于无法使用。 - Ami Tavory

5
我建议使用enable_if_t,如果你要传入任何单个字符变量,请对其进行专门化处理:
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);
}

这里我只专门处理char。如果您需要专门处理wcharchar16char32,您也需要这样做。

对于非算术类型,这些重载将默认使用ostringstream,这很好,因为如果您重载了其中一个类的提取运算符,它就会处理它。

对于算术类型,除了char和其他任何您重载的内容之外,它将使用to_string,并且可以直接创建一个string

编辑:

Dyp建议使用enable_if_t作为我的条件,判断to_string是否接受T::type参数。

最简单的解决方案仅适用于您是否可以访问#include < experimental / type_traits>中的is_detected。 如果您可以,只需定义:

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

然后你可以将你的代码设置为:
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);
}

我问了这个问题来找出如何将to_string用作我的条件。如果您无法访问is_detected,我强烈建议阅读一些答案,因为它们非常出色:元编程:函数定义的失败定义了一个单独的函数

请随意“借鉴”。无需基于SFINAE再添加另一个答案。 - dyp
@dyp 这似乎是个好主意,但当我开始实现时,我无法想出如何编写完全相反的代码。我该如何表达:“如果 to_string<T> 未定义,则返回字符串”? - Jonathan Mee
你可以将尾返回类型转换为特质类,或添加一个虚拟参数以排序重载。后者:template<typename T> string stringify(T&& t) { return stringify(forward<T>(t), 0); } template<typename T> auto stringify(T&& t, int) -> decltype(to_string(forward<T>(t))); template<typename T> string stringify(T&& t, ...); 更高级的方法可以在这篇博客文章中找到。 - dyp
@dyp 看起来应该有更简单的方法来完成这个任务。我在这里添加了一个问题(https://dev59.com/2ddR0ogBFxS5KdRjvYjU),你可能想要参与讨论。 - Jonathan Mee

3
最简单的解决方案是针对你想要的类型进行重载:
using std::to_string;

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

由于to_string不是模板,所以您无法对其进行特化,但幸运的是这更容易。

该代码假定数组以空结尾,但如果没有空结尾仍然是安全的。

如果您对using放置的位置感到强烈,则还可以将其放在调用to_string的函数内部。

这也有一个好处,即如果您以某种方式传递了非空结尾字符串,则它不会像一个参数std::string构造函数那样具有未定义行为。


你是否需要 Size - 1 取决于它是否以 NUL 结尾。因此,你的代码可以进行检查。 - jxh
我本来打算这样做(即根据arr [Size-1]进行选择),但是如果字符串包含空值,其中一个恰好在末尾,它将剪掉最后一个并可能引起问题。 - Dan
我有些困惑。如果我想要存储一个包含 '\0' 的二进制字节,你的代码将无法存储它。如果我想要存储一个包含 '\a' 的单个二进制字节,你的代码也无法存储它。 - jxh
这个解决方案并不完美。请查看我的答案以获取更多细节。 - dened

3

我相信,最优雅的解决方案是:

#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
}

在这里,如果我们可以使用T构造std::string(我们通过std::is_constructible<std::string, T>进行检查),那么我们就这样做,否则我们使用to_string
当然,在C++14中,你可以用更短的std::enable_if_t<...>代替typename std::enable_if<...>::type。下面是代码的较短版本示例。
下面是一个较短的版本,但它的效率稍微低一些,因为它需要对std::string进行额外的移动(但如果我们只做一个复制,效率会更低)。
#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
}

此版本在可能的情况下使用隐式转换为std::string,否则使用to_string。请注意使用std::move来利用C++11的移动语义

这就是为什么我的解决方案比当前最受欢迎的解决方案更好,由@cerkiewny提供:

  • 它具有更广泛的适用性,因为通过ADL,它也适用于任何定义了使用函数to_string进行转换的类型(不仅限于std::版本),请参见下面的示例用法。而@cerkiewny的解决方案仅适用于基本类型和可以构造为std::string的类型。

    当然,在他的情况下,可以添加其他类型的额外重载stringify,但如果与添加新的ADL版本的to_string相比,则这是一个不太可靠的解决方案。而且很有可能,ADL兼容的to_string已经在第三方库中为我们要使用的类型定义了。在这种情况下,使用我的代码,您根本不需要编写任何其他代码即可使stringify正常工作。

  • 它更有效率,因为它利用C++11的完美转发(通过使用通用引用(T&&)和std::forward)。

示例用法:

#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)"
}

备选方案,但不推荐使用

我也考虑过仅仅重载 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
}

还可以使用隐式转换为std::string的简短版本:

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

但是这种方法有严重的局限性:我们需要记住在每个想要使用它的地方写to_string而不是std::to_string;并且它与最常见的ADL使用模式不兼容。
int main() {
    std::string a = std::to_string("Hello World!"); // error

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

很可能,这种方法还存在其他问题。

在与此方法相关的其他问题上。

我喜欢使用is_constructible,我之前不知道它的存在。 - cerkiewny
使用clang编译器,使用std::enable_if比使用std::enable_if_t能够获得更好的错误信息。 - Paul Fultz II
1
对于许多人来说,为什么ADL在这里很重要可能并不明显,但是类似于但显然不同于this的内容添加到您的答案中将会有所改善。 - Shafik Yaghmour

2

虽然这个问题不是一个“给我代码”的问题,因为我已经有了一个解决方案,所以我想分享一下:

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...);
}

使用方法:

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;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接