How to write a method to cout elements (of user-defined type) from sequential containers?

I'm designing a little function for general purpose use. The function is called StrItems() and its job is to return the string-representation of a sequential container (array, vector, queue, deque, etc.)

I've used std::to_string to do the type conversion but to_string has a small number of overloads for primitive types.

I'm not very familiar with template programming so I was wondering how I could write this function so it will accept sequential containers of user-defined types.

Here's what it looks like so far:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Returns a string representation of the container as a space-separated concatenation of its
// elements. If newline is true, appends newline to the end of the string. The string is
// preceded by (indentLevel * indentWidth) number of indentChars. 
template <class T>
std::string StrItems(T container, bool newline = true, int indentLevel = 0, int indentWidth = 2, char indentChar = ' ')
{
   if (container.empty()) return "";

   std::string repr = std::string(indentWidth * indentLevel, indentChar);
   for (auto it = container.begin(); it != container.end(); it++) {
      repr.append(std::to_string(*it) + " ");
   }
   if (newline) 
     repr.back() = '\n';
   else 
     repr.erase(repr.end() - 1); // Removes the trailing space  
   return repr;
}


And some code to exercise it:

1
2
3
4
5
6
7
8
int main()
{
   std::vector<int> v1 = {1,2,3,4,5};
   std::cout << StrItems(v1, true); 
   
   std::cin.get();
   return 0;
}
Last edited on
In C++, I usually see more of a focus on streams than you might in other languages.
Usually, the primary functionality desired is to be able to send data to a stream, whether it's cout (stdout), a file stream, or a string stream. A stringstream, for example, can then easily be converted into a string.

But if you're just going to be printing the value, converting it to a string first is just wasted computation -- just directly send the object/container to the output stream.

For example, here's a possible setup. Not saying it's the best, but it just demonstrates what I mean, hopefully:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
#include <string>
#include <sstream>

// custom type
struct Foo { int data; };

// custom way to send this type to an output stream
std::ostream& operator<<(std::ostream& os, const Foo& foo)
{
    return os << foo.data;
}

template <typename T>
std::ostream& print_container(std::ostream& os, const T& container) // etc. (format options)
{
    for (const auto& item : container)
    {
        os << item << '\n';
    }

    return os;
}

template <typename T>
std::string strItems(const T& container)
{
    std::ostringstream oss;
    print_container(oss, container);
    return oss.str();
}

#include <vector>
#include <list>

int main()
{
    using namespace std;
    
    list<Foo> foo_list = { {1}, {2}, {5} };
    vector<Foo> foo_vec = { {40}, {41}, {42} };
    vector<int> int_vec = {9, 10, 11, 12};
    
    print_container(cout, foo_vec) << '\n'; // directly sending to cout
    cout << strItems(foo_vec) << '\n'; // converting to string, then printing
    cout << strItems(foo_list) << '\n'; //   (a bit redundant in these cases)
    cout << strItems(int_vec) << '\n';
}


However, I wouldn't say it's wrong to use to_string. But a little clever trick is needed if you want to_string to work for custom types:
I've used std::to_string to do the type conversion but to_string has a small number of overloads for primitive types.
You can always make your own to_string function for your custom data type. The trick is that inside your function template, you put the line using std::to_string;

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
26
// Example program
#include <iostream>
#include <string>

class Foo { } ;

std::string to_string(const Foo& foo)
{
    return "abc"; 
}

template <typename T>
std::string my_func(const T& obj)
{
    using std::to_string;
    return to_string(obj);
}

int main()
{
    int a = 42;
    Foo foo;
    
    std::cout << my_func(a) << '\n';
    std::cout << my_func(foo) << '\n';
}

This allows the compiler to deduce whether or not it actually needs to call the std::-version of to_string or a custom non-std "overload".

My first code allows directly sending the container to an output stream like cout instead of converting things to strings first.

tl;dr
Either define your own custom operator<< overload, or define your own custom to_string function. The advantage of doing the operator<< way is that you can directly send objects to streams instead of converting them to strings first.
Last edited on
I don't feel the indent or newline are particularly useful.
However, allowing the user to specify the separator would be useful.

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
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <string>
#include <vector>

class Object {
    int n;
public:
    Object(int n) : n(n) { }
    int get() const { return n; }
};

std::string to_string(const Object& o) {
    return std::to_string(o.get());
}

template<typename T>
std::string join(const T& c, std::string sep = " ")
{
    using std::to_string;
    if (c.empty()) return "";
    std::string s;
    auto it = c.begin();
    s = to_string(*it);
    for (++it; it != c.end(); ++it) s.append(sep + to_string(*it));
    return s;
}

int main()
{
    std::vector<int> v = {1, 2, 3, 4, 5};
    std::cout << join(v, ", ") << '\n';
    
    std::vector<Object> w = {10, 20, 30, 40, 50};
    std::cout << join(w, ", ") << '\n';
}

Ganado wrote:
In C++, I usually see more of a focus on streams than you might in other languages.
Usually, the primary functionality desired is to be able to send data to a stream, whether it's cout (stdout), a file stream, or a string stream. A stringstream, for example, can then easily be converted into a string.

But if you're just going to be printing the value, converting it to a string first is just wasted computation -- just directly send the object/container to the output stream.


I see.

I come from C# where it's string-centric than operator-centric. By that I mean in C#, all classes inherit from a special base class, Object [1], which define a virtual method string ToString(). Subclasses (i.e., user-defined classes) inherit the method and developers override them to define how their classes are represented as strings. In contexts where the object is used like a string (e.g., System.Console.Printline(MyObject)), the overloaded MyObject.ToString() gets called. Python classes work similarly with __repr__ and __str__ methods, [2].

I tried implementing something similar to your second suggestion but wasn't sure how to tell the compiler to "use std::to_string unless the type defines its own to_string()":

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Foo { } ;

std::string to_string(const Foo& foo)
{
    return "abc"; 
}

template <typename T>
std::string my_func(const T& obj)
{
    using std::to_string;
    return to_string(obj);
}

...
This allows the compiler to deduce whether or not it actually needs to call the std::-version of to_string or a custom non-std "overload".


Can you clarify what's happening here:

1
2
    using std::to_string;
    return to_string(obj)


My guess is that execution tries to run to_string(obj). If the type of obj matches one defined by std::to_string(), the std::'s to_string() gets called. Otherwise, it looks for a function with a matching signature across all other compilation units (which it will find, presumably in the header/src file containing your Foo class.

[1] https://docs.microsoft.com/en-us/dotnet/api/system.object?view=netcore-3.1
[2] https://www.journaldev.com/22460/python-str-repr-functions
Last edited on
dutch wrote:
I don't feel the indent or newline are particularly useful.
However, allowing the user to specify the separator would be useful.


Thanks for the suggestion. Separator char would be a useful optional argument. I thought the indentation would be useful for console-output, in particular when you you're using it to indicate when an action has occurred inside another action (or in recursive calls):


Making Tea
.Boil Water
..Pour water into electric kettle
...Make sure kettle is unplugged
...Pour water into kettle reservoir
..Plug kettle in
.Find tea bag
..Search cabinet 1
..Search cabinet 2


dutch wrote:

using std::to_string;
if (c.empty()) return "";
std::string s;
auto it = c.begin();
s = to_string(*it);


I see. This is similar to Ganado's second suggestion. It's a fine technique.
Last edited on
now try
1
2
std::vector<std::vector<int> > vv{ {1,2}, {30,40} };
StrItems(vv);
ElusiveTau wrote:
My guess is that execution tries to run to_string(obj). If the type of obj matches one defined by std::to_string(), the std::'s to_string() gets called. Otherwise, it looks for a function with a matching signature across all other compilation units (which it will find, presumably in the header/src file containing your Foo class.
Well, it's not "execution" that does it. The overload resolution is all determined at compile-time.
But yes, you have the gist of it.

1
2
    using std::to_string;
    return to_string(obj);

The using call allows "to_string" to be able to refer to any "to_string" (global namespace) or "std::to_string" (std namespace) function. When generating the function from the function template, the compiler then sees that a function with a signature "to_string(Foo)" is being called, so it tries to find a match for that function signature, which it can now that to_string(const Foo&) has been defined by the programmer.

See also: https://stackoverflow.com/questions/4782692/what-does-using-stdswap-inside-the-body-of-a-class-method-implementation-mea
Last edited on
Topic archived. No new replies allowed.