简体   繁体   中英

What is the preferred way to represent data structures as strings in C++?

What is the preferred way to represent a data structure in C++? I would like to utilize strings, although if a better option is available, I would be willing to try that.

I've been creating a data structure for use in a larger program. The structure is supposed to represent something akin to a Python dictionary. I want to print the entire data structure as a string. Is there a standard way to do this in C++? The guidelines suggested using the to_string() function, however I'm not sure if that works as I want.

I'm using template classes, and am unsure that there is a one-off solution to handle whatever combination of Key's / Value's are thrown at the program.

I've been testing this so far with char 's.

// Table class
std::string toString()
  {
    std::string result = " { ";
    ListNode<Key, Value>* current = this->table;
    while(current != nullptr)
    {
      result += current->toString();
      current = current->next;
    }
    result += " } ";
    return result;
  }

// List Class
std::string toString()
  {
    std::string result = "";
    result += std::to_string(this->key);
    result += " : ";

    Node<Value>* current = this->list;

    while(current != nullptr) // changed to be current, not list
    {
      result += std::to_string(current->value);
      current = current->next;
    }

    return result; // added return
  }

This function works more like a builder for the particular string. It goes into the smaller data structures calling similar functions to get to the base.

I expected to have something like:

{ Key1 : value1, value2, value3, Key2: value1, Key3: value1, value2 }

I've just been getting { }

Stringify-ing Everything

I think it'd be good for you to see an example of how this would work in practice. String-ification should be fast , easy , and simple . Unfortunately, it's not built in to the standard library.

Nevertheless, in a little over 100 lines of code, we can write code to stringify std::map , std::tuple , std::unordered_map , std::set , std::unordered_set , std::list , std::vector , std::pair , std::tuple , and any combination thereof. So, for example, you could have a tuple containing a vector of unordered_maps between ints and strings, among other misc. stuff:

std::vector<std::unordered_map<int, std::string>> vect {
    {{10, "Hello"}, {20, "World"}},
    {{1, "1"}, {2, "2"}, {3, "3"}},
    {{1000, "thousand"}, {1000000, "million"}},
};

// If we can print this, we're GOLDEN
auto justTryAndPrintMe = std::tuple{10, 20, vect, 0.3f, short(3), 2398987239890ll}; 

Because of it's design, the interface we write will be easy to extend to custom types as well.

Describing the interface

The interface has three parts.

  • to_string overloads (this is provided by std::to_string )
  • append_to overloads (we write these)
  • One stringify function template, that calls append_to .

Let's look at the stringify function first:

namespace txt 
{
    template<class T>
    std::string stringify(T const& t) 
    {
        std::string s;
        append_to(s, t); 
        return s; 
    }
}

Why use append_to ? Appending to a string is very fast. When you're appending multiple times, std::string reserves more capacity than is actually needed, so any single append is unlikely to result in a new allocation because the string will have enough capacity to fit the object. This reduces calls to new and delete .

Writing the interface.

append_to has a fallback, which uses to_string , along with a number of specializations for different containers. The specializations will be forward-declared, so that they're visible to each other.

Fallback.

This version the least specialized, and it's only enabled if there's no other way to append the object. We use SFINAE to check for the existence of a to_string method.

This to_string method can come from namespace std; it can be defined in the global namespace, or to_string can even be defined in the same namespace as a user-defined type. In the last case, the compiler finds it through ADL (Argument Dependent Lookup).

namespace txt {
    using std::to_string;

    // This nasty bit in the template declaration is the SFINAE
    template<class T, class = decltype(to_string(std::declval<T>()))>
    void append_to(std::string& s, T const& obj)
    {
        s += to_string(obj); 
    }
}

Special cases

There's to to_string function for std::string or cstrings, but it's simple to write efficent append functions for those.

// namespace txt

template<size_t N>
void append_to(std::string& s, char const(&str)[N]) {
    s.append(str, N - 1); 
}
void append_to(std::string& s, std::string const& s2) {
    s += s2; 
}

Forward-declarations of STL containers and types

It's a good idea to forward-declare these as doing so will allow them to call each other.

// namespace txt

template<class... T>
void append_to(std::string& s, std::tuple<T...> const& t); 
template<class F, class S>
void append_to(std::string& s, std::pair<F, S> const& p); 
template<class T>
void append_to(std::string& s, std::vector<T> const& v); 
template<class T>
void append_to(std::string& s, std::set<T> const& v); 
template<class T>
void append_to(std::string& s, std::list<T> const& v); 
template<class Key, class Value>
void append_to(std::string& s, std::map<Key, Value> const& v); 
template<class T>
void append_to(std::string& s, std::unordered_set<T> const& v); 
template<class Key, class Value>
void append_to(std::string& s, std::unordered_map<Key, Value> const& v); 

Handling ranges

This is pretty straight-forward. We'll write an append_range function that takes a begin and an end iterator, and we ll use the append_range function to implement append_to` for the STL containers.

// namespace txt

template<class It>
void append_range(std::string& s, It begin, It end) {
    if(begin == end) {
        s += "{}"; 
    } else {
        s += '{';
        append_to(s, *begin); 
        ++begin;
        for(; begin != end; ++begin) {
            s += ", "; 
            append_to(s, *begin); 
        }
        s += '}'; 
    }
}

Handling pairs and tuples

We can use std::apply (introduced in C++17) to expand the tuple into a parameter pack. Once we have that, we can use a fold expression to append each of the elements.

// namespace txt

template<class... T>
void append_to(std::string& s, std::tuple<T...> const& t) 
{
    if constexpr(sizeof...(T) == 0) 
    {
        s += "{}"; 
    } 
    else 
    {
        // This is a lambda. We'll use it to append the tuple elements. 
        auto append_inputs = [&s](auto& first, auto&... rest) 
        {
            // Append the first one
            append_to(s, first); 
            // Append the rest, separated by commas
            ((s += ", ", append_to(s, rest)) , ...); 
        }; 

        s += '('; 
        std::apply(append_inputs, t); 
        s += ')'; 
    }
}

Appending a pair is pretty straight-forward as well:

// namespace txt

template<class F, class S>
void append_to(std::string& s, std::pair<F, S> const& p) 
{
    s += '(';
    append_to(s, p.first); 
    s += ", "; 
    append_to(s, p.second); 
    s += ')'; 
}

Handling STL containers

For all of these, we just call append_range on the beginning and ending iterators.

template<class T>
void append_to(std::string& s, std::vector<T> const& v) {
    append_range(s, v.data(), v.data() + v.size()); 
}
template<class T>
void append_to(std::string& s, std::set<T> const& v) {
    append_range(s, v.begin(), v.end()); 
}
template<class Key, class Value>
void append_to(std::string& s, std::map<Key, Value> const& v) {
    append_range(s, v.begin(), v.end()); 
}
template<class T>
void append_to(std::string& s, std::unordered_set<T> const& v) {
    append_range(s, v.begin(), v.end()); 
}
template<class Key, class Value>
void append_to(std::string& s, std::unordered_map<Key, Value> const& v) {
    append_range(s, v.begin(), v.end()); 
}

Custom types: as easy as writing a function

This is really simple. We can just write an append_to method for the type. It can go after you include the header file containing namespace txt . ADL finds it automagically.

namespace foo {
    struct MyVec {
        int x;
        int y;
        int z;
    }; 
    void append_to(std::string& s, MyVec v) {
        using txt::append_to; 

        s += '<'; 
        append_to(s, v.x);
        s += ", "; 
        append_to(s, v.y);
        s += ", ";  
        append_to(s, v.z); 
        s += '>'; 
    }
}

Click here to run the code

Using this code in your library

We can write an append_to function for Table pretty easily!

namespace YourNamespace {
// Here, I forward declare append_to for table 
// Forward-declaring it allows you to have tables of tables
template <class Key, class Value>
void append_to(std::string& s, Table<Key, Value> const& table); 

template <class Key, class Value>
void append_to(std::string& s, ListNode<Key, Value> const& node) 
{
    // We need to use the txt version so it's included in our overload set
    using txt::append_to; 

    append_to(s, node.key); 
    s += " : "; 
    append_to(s, node.value);  
}

template <class Key, class Value>
void append_to(std::string& s, Table<Key, Value> const& table) 
{
    using txt::append_to; 

    ListNode<Key, Value> const* current = table.table;
    if (current == nullptr) 
    {
        s += "{}";
    } 
    else 
    {
        s += "{ ";
        append_to(s, *current);
        current = current->next;
        for (; current != nullptr; current = current->next) {
            s += ", ";
            append_to(s, *current);
        }
        s += " }";
    }
}
} // namespace YourNamespace

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM