2

How create a generic stringify function for a generic container also for nested container e.g. map<string,vector<list<int>>>? This is my attempt but it doesn't work.

template<class T>
string stringify(const T& c) {
    int length = c.size();
    string str = "[";
    int i=0;
    for (; i <= length-2; i++) {
        str += stringfy(c[i]) + ", ";
    }
    str += c[i] + "]";
    return str;
}
asv
  • 2,164
  • 2
  • 17
  • 35
  • Why doesn't your attempt work? – user253751 Dec 06 '19 at 15:26
  • Sorry for a maybe naive comment, but what would be `c[i]` if `c`is a `map>>` ? – Damien Dec 06 '19 at 15:27
  • @user253751 Oups - My bad. – Ted Lyngmo Dec 06 '19 at 15:29
  • 3
    In general you can't do this. You need to make a `stringify` that works for iterable types, then you need a stringify that works for fundemental types, and then you need a `stringify` for each custom type (`std::pair`, you own classes). – NathanOliver Dec 06 '19 at 15:31
  • 1
    With what @NathanOliver mentioned, one can get close by using `std::ostringstream` and a number of templates. If every type is streamable, it's feasable, but it won't be a "works in all cases" solution. – Ted Lyngmo Dec 06 '19 at 15:39
  • @Ted I would like to see a code of your idea – asv Dec 06 '19 at 15:40
  • @Damien You're right in that case the type of 'i' and the key of map mismatch – asv Dec 06 '19 at 15:42
  • @asv Ok - I'll put something together when I get to a computer (4-8 hours). – Ted Lyngmo Dec 06 '19 at 15:42
  • See this for some ideas: https://stackoverflow.com/questions/4850473/pretty-print-c-stl-containers – NathanOliver Dec 06 '19 at 15:43
  • Because this is a template, you may have difficulty encoding the template instantiation type. Therefore you might find it easier to explicitly make a wrapping function for each type of T, then printing out the encoded value of the type, within the string, followed by the string encoding, this will allow you to reread the data back afterwards into a map. You can still call the templated function within the typed implimented wrapper functions. – Owl Dec 06 '19 at 15:52
  • @asv I think I don't need to take it further, right? The "Pretty-print C++ STL containers" link kind of did it? – Ted Lyngmo Dec 07 '19 at 00:38
  • 1
    @TedLyngmo I tried here: https://repl.it/repls/UnderstatedShortCgi but it doesn't work – asv Dec 07 '19 at 07:56
  • @asv I remembered that I made a promise ... then beer happened, but I came to deliver ... and you let me off the hook. Brilliant! – Ted Lyngmo Dec 07 '19 at 13:02

2 Answers2

3

It is doable but mostly pointless as you would typically know what kind of data you have to process.

I managed to come up with something like this. It will work for every type that is iterable with for-each loop or a tuple, or has overloaded operator<<. You can do it without C++20 features but it will be a total SFINAE mess.

#include <iostream>
#include <string>
#include <type_traits>
#include <map>
#include <list>
#include <vector>
#include <tuple>
#include <sstream>

using namespace std;

template <typename T>
concept InterableRange = requires (T a) {
    std::begin(a);
    std::end(a);
};

template <typename T>
concept TupleLikeType = requires (T a) {
    std::tuple_size<T>();
};

template<TupleLikeType T>
string stringify(const T& c);

template<class T>
string stringify(const T& c);

template<InterableRange T>
string stringify(const T& c) {
    string str = "[ ";
    auto size = std::size(c);
    std::size_t i = 0;
    for (const auto& elem : c) {
        str += stringify(elem);
        if(i++ < size - 1)
            str += ", ";
    }
    str += " ]";
    return str;
}

template<TupleLikeType T>
string stringify(const T& c) {
    string str = "[ ";
    auto size = std::tuple_size<T>();
    std::size_t i = 0;

    std::stringstream input;   
    auto insert = [&input, size, &i](const auto& data) {
        input << stringify(data);
        if(i++ < size - 1)
        {
            input.put(',');
            input.put(' ');
        }
    };
    std::apply([&insert](const auto&... args){
        (insert(args), ...);
    }, c);

    str += input.str();
    str += " ]";
    return str;
}

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


int main() {
    map<string,vector<list<int>>> m {
        { "1", {{1,2}, {3, 4}}},
        { "2", {{10,20}, {30, 40}}}
    };
    cout << stringify(m);
}

It will print

[ [ [ 1 ], [ [ 1, 2 ], [ 3, 4 ] ] ], [ [ 2 ], [ [ 10, 20 ], [ 30, 40 ] ] ] ]
Rinat Veliakhmedov
  • 1,081
  • 15
  • 32
  • I tried here: https://repl.it/repls/AvariciousVagueDriverwrapper and it doesn't work. – asv Dec 07 '19 at 16:42
  • 1
    @asv Because it uses clang7 and you need a C++20 compiler that supports concepts: https://godbolt.org/z/VVHbzh Unfortunately C++20 support is not very good yet as it was only recently finalized. – Rinat Veliakhmedov Dec 07 '19 at 21:25
3

I did a C++17 solution, the SFINAE is still bearable:

#include <iostream>
#include <set>
#include <map>
#include <string>
#include <vector>
#include <sstream>


// Forward declarations so we can have arbitrary interactions
template<class Container, class Iter = decltype(cbegin(std::declval<Container>()))> // SFINAE to get only containers
std::string stringify(const Container&c); 
template<class T1, class T2>
std::string stringify(const std::pair<T1, T2> &p); // Can we get this into the tuple case?
template<class ...Ts>
std::string stringify(std::tuple<Ts...> &t);

template<class T, class = decltype(std::declval<std::stringstream>() << std::declval<T>())>
std::string stringify(T t) {
    std::stringstream s;
    s << t;

    return s.str();
}

template<class ...Ts>
std::string stringify(std::tuple<Ts...> &t) {
    const auto string_comma = [] (const auto & arg) { return stringify(arg) + ", "; };
    // This prints a , too much but I am too lazy to fix that
    return '(' + std::apply([&] (const auto& ...args) { return (string_comma(args) + ...); }, t) + ')'; 
}

template<class T1, class T2>
std::string stringify(const std::pair<T1, T2> &p) {
    return '(' + stringify(p.first) + ", " + stringify(p.second) + ')';
}

template<class Iter> 
std::string stringify(Iter begin, Iter end) {
    std::string ret{'['};
    for(; begin != end;) {
        ret += stringify(*begin);
        if(++begin != end) {
            ret += ", ";
        }
    }
    ret+=']';
    return ret;
}

template<class Container, class Iter>
std::string stringify(const Container&c) {
    return stringify(cbegin(c), cend(c));
}

int main () {
    std::set<std::vector<std::map<int, char>>> v {{{{1, 'A'}, {2, 'E'}}, {{2, 'B'}}}, {{{2, 'C'}}, {{3, 'D'}}}};
    std::tuple tup {1.0, "HELLO WORLD", std::pair{67, 42}};

    std::cout << stringify(begin(v), end(v)) << '\n';
    std::cout << stringify(tup) << '\n';

}
n314159
  • 4,701
  • 1
  • 3
  • 20
  • I tried here https://repl.it/repls/FlimsyLongtermStruct and it doesn't work. – asv Dec 07 '19 at 16:44
  • 1
    @asv You are trying to compile it with C++14, but it is C++17 code. To make it C++14 compatible, one would have to do the stringification of tuples in a template-recursive way, since C++14 does not have `std::apply` and cannot use parameter pack fold expressions. – n314159 Dec 07 '19 at 21:03