如何将各种类型的向量转换为std::string?

3
我从转向C++,这两种语言都有内置的方法将数据类型转换为字符串。
例如,在Haskell中,有多态函数show
我想在C++中创建一些类似的模板函数。
例如,我们可以将vector<int>转换为字符串,如下所示。
string toString(vector<int> v)
{
    ostringstream o;
    for (int elem: v)
        o << elem << " ";
    return o.str()
}

这将把所有int的字符串表示都放在一行上。现在,如果我想以同样的方式转换一个vector<vector<int> >,该怎么办?
string toString(vector<vector<int> > v)
{
   ostringstream o;
   for (auto elem : v)
   {
      o << toString(elem) << "\n";
   }
}

我的问题是:如果我想创建一个多态的toString,它可以与vector<class A>vector<vector<class A>一起使用,该怎么办?我应该添加一些功能来将类型class A转换为std::string吗?我只需要提供至少一个专门为该类型的toString的特化吗?模板机制会自动解决所有这些吗?还是已经有代码可以做到这一点了?

2
如果你只是用通用的 T 替换 int,也就是将这两个函数模板化,那应该就可以了。 - cigien
“Polymorphic”指的是类,而不是函数。更好的词选择应该是“通用”的 toString - JaMiT
通过明智地使用模板和技术(如SFINAE)或C++20中的概念,可以在一定范围内做到这样的事情。这些都是相当高级、深入的主题。对于刚开始学习C++的人来说,可能还没有准备好应对这些“野兽”,继续学习这门当今最复杂的通用编程语言将更具生产力,直到上述技术变得轻车熟路(不,还没有现成的代码可以做到这一点)。 - Sam Varshavchik
你能添加一些你想要运行的示例代码吗? - cigien
@JaMiT 这只是C++吗?因为我理解Haskell将泛型函数称为多态,也许其他语言也是这样。 - composerMike
@composerMike 不仅仅是C++,同时Haskell的术语也不仅仅是Haskell。两者的术语都不是普遍适用的。 - JaMiT
2个回答

3

目前尚无直接通用的方法来实现此操作,但您可以自己构建。以下是一个示例程序,它将模仿您所需的行为。

#include <exception>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>

template<typename T>
std::string toString(const std::vector<T>& vec) {
    std::ostringstream stream;
    for (auto& elem : vec) {
        stream << elem << " ";
    }
    stream << '\n';
    return stream.str();
}

template<typename T>
std::string toString(const std::vector<std::vector<T>>& vec) {
    std::ostringstream stream;
    for (auto& elem : vec) {
        stream << toString(elem);
    }
    stream << '\n';
    return stream.str();
}


int main() {
    try {
        std::vector<int> valuesA{ 1, 2, 3, 4 };
        std::cout << toString(valuesA) << '\n';

        std::vector<std::vector<float>> valuesB { {1.0f, 2.0f, 3.0f},
                                                  {4.0f, 5.0f, 6.0f},
                                                  {7.0f, 8.0f, 9.0f}
                                                };
        std::cout << toString(valuesB) << '\n';
    } catch( const std::exception& e ) {
        std::cerr << "Exception Thrown: " << e.what() << std::endl;
        return EXIT_FAILURE;
    } catch( ... ) {
        std::cerr << __FUNCTION__ << " Caught Unknown Exception" << std::endl;
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

输出

1 2 3 4

1 2 3
4 5 6
7 8 9

上述代码适用于存储类型为vector<T>vector<vector<T>>的容器,但并非所有情况都适用。如果您有一个嵌套在另一个向量中的向量,则函数声明将无法识别它。此外,它也无法识别其他容器类型,例如mapssetslistsqueues等...这时,您就需要生成可以接受所有不同类型容器的函数...
此时,您会开始看到代码重复和重复模式。因此,与其将函数声明为:
template<T>
std::string toString(const std::vector<T>& vec) { /* ... */ }

您可以对“容器(container)”本身进行模板化...
template<template<class> class Container, class Ty>
std::string toString(const Container<Ty>& container ) { /*... */ }

现在这个方法适用于大多数容器,但有些容器可能会比较棘手,比如 std::map ,因为它可以从一个 std::pair 中获取值,或者根据其声明连同使用花括号初始化的构造函数取两个对应类型。这就是你可能需要特定于该容器重载函数的地方,但总体思路仍然适用。
这不仅仅使用了templates,而且还使用了其参数本身也是templates,如果您不熟悉它们,则其语法可能对初学者有点困难。我相信您可以在template template参数上找到大量研究...
编辑:
另外,仍需注意传递给 Container<Ty>type。对于简单的内置类型,如 intfloatchardouble 等,这很简单...
但是,如果您有自己定义的用户classstruct呢...
class Foo {
private:
    int bar;
    float baz;
public:
    Foo() : bar{0}, baz{0.0f} {}
    Foo(int barIn, float bazIn) : bar{barIn}, baz{bazIn} {}
};

然后您或其他人尝试使用您的代码并执行以下操作:

std::vector<Foo> foos { Foo(1, 3.5f), Foo(2, 4.0f), Foo(3, 3.14159f) };
std::string report = toString(foos);

以上问题并不容易解决,因为程序或函数不知道如何将 Foo 转换为 std::string。所以需要谨慎考虑。在这种情况下,您可能需要额外的帮助模板函数来将用户定义的类或结构转换为 std::string,然后您需要为这些类型专门编写 toString() 函数并在其中使用转换辅助函数...


现在,随着每个标准版本和各种编译器的改进,C++ 语言正在不断发展,事情往往变得更加简化,这意味着这很快就会成为一种常见的重复模式,并最终变得流畅。对于 C++ 的未来前景是积极的。已经有工具可以帮助您构建自己的工具。随着时间的推移,这些工具变得易于访问,并且甚至可以简化您的代码和生产时间。


2
在C++17中似乎是可能的(在旧版本中需要写更多代码)。 - JeJo
1
随着C++语言标准的每一次迭代以及编译器的改进,事情往往变得更加简化...例如,在c++11之前,我们可能会有std::vector<int> vec{1,2,3,4}; for (size_t i = 0; i < vec.size(); i++) { std::cout << vec[i] << ' '; },现在我们可以轻松地做到:for(auto &e : vec) { std::cout << e << ' ';}。因此,请考虑到C++20和未来的版本,重新审视您的猜测。 - Francis Cugler

3
如果我想创建一个多态的toString,它可以与vector<class A>vector<vector<class A>一起使用,我该怎么做呢?是可以实现的,在中,通过使用if constexpr特性和递归函数模板(即将toString作为递归函数模板)来实现。在进入通用函数模板之前,你的class A需要实现operator<<重载,以便std::ostringstream::operator<<可以使用它。例如,让我们考虑一下。
struct A
{
   char mChar;
   // provide a overload for operator<< for the class!
   friend std::ostream& operator<<(std::ostream& out, const A& obj) /* noexcept */ {
      return out << obj.mChar;
   }
};

现在,toString 函数看起来应该像以下内容:
#include <type_traits> // std::is_floating_point_v, std::is_integral_v, std::is_same_v
                       // std::remove_const_t, std::remove_reference_t

template<typename Type>
inline static constexpr bool isAllowedType = std::is_floating_point_v<Type>
|| std::is_integral_v<Type> 
|| std::is_same_v<A, Type>;
//^^^^^^^^^^^^^^^^^^^ --> struct A has been added to the
//                        allowed types(i.e types who has operator<< given)

template<typename Vector>
std::string toString(const Vector& vec) /* noexcept */
{
   std::ostringstream stream; 
   // value type of the passed `std::vector<Type>`
   using ValueType = std::remove_const_t< 
      std::remove_reference_t<decltype(*vec.cbegin())>
   >;
   // if it is allowed type do  concatenation!
   if constexpr (isAllowedType<ValueType>) 
   {
      for (const ValueType& elem : vec)
         stream << elem << " ";
 
      stream << '\n';
      return stream.str();
   }
   else
   {
      // otherwise do the recursive call to toString
      // for each element of passed vec
      std::string result;
      for (const ValueType& innerVec : vec)
         result += toString(innerVec);

      return result; // return the concatenated string
   }   
}

现在,您可以调用toString方法来处理std::vector<std::vector<A>>std::vector<A> aObjs,以及std::vector< /* primitive types */ >

(查看完整在线演示)


我只需要为该类型提供至少一个toString的具体实现吗?模板机制会自动解决这些问题吗?
模板特化也是另一种选择。然而,如果您可以使用C++17,我建议采用上述方式,它将解决您在问题中提供的所有类型。

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