3
\$\begingroup\$

I was mad C++ did not have support for printing containers. I also could not find a header only library for printing containers so I decided to make my own. My goals were: 1) practicing templates 2) learn how to make a small library. I would also be very happy if some people found this useful and used it. What does this library offer:

  • Header only, so should be really easy to setup this library (just put the .h file in your project)
  • Has support for printing the most common types
  • Easy to use ( has only one function print )

With my goals in mind can you please critique this code. And maybe some general advice?

#include <iostream>
#include <tuple>
#include <utility>

namespace p {
   template<typename T> // forward dec
   void print(const T& t, const std::string& sep = ", ", const std::string& end = "\n");

   namespace {
      // Requires T to have ::iterator and ::const_iterator, which should 
      // guarantee that the type is iterable as far as the STL is concerned
      template<typename T>
      concept Iterable = requires(T a) {
         typename T::iterator;
         typename T::const_iterator;
      };

      // Forward declarations
      template<typename T>
      void print_iterable(const T& t, const std::string& sep);

      void print_iterable(const std::string& str, const std::string& sep);

      void print_iterable(const bool b, const std::string& sep);

      template<typename TupType, size_t... I>
      void print_iterable(const TupType& tup, std::index_sequence<I...>, const std::string& sep);

      template<typename... T>
      void print_iterable(const std::tuple<T...>& tup, const std::string& sep);

      template<class T1, class T2>
      void print_iterable(const std::pair<T1, T2>& pair, const std::string& sep);

      template<Iterable T>
      void print_iterable(const T& t, const std::string& sep);


      // ----------------- End of forward declartions

      // Print a non-iterable type
      template<typename T>
      void print_iterable(const T& t, const std::string& sep)
      {
         std::cout << t;
      }

      // Specialization for std::string (should be treated like non-iterable)
      void print_iterable(const std::string& str, const std::string& sep)
      {
         std::cout << str;
      }

      // Print bools
      void print_iterable(const bool b, const std::string& sep) {
         std::cout << std::boolalpha << b;
      }

      // Helper to put sep string between print statements inside fold expression
      template<typename T>
      void print_with_delim(const T& t, size_t I, size_t index_length, const std::string& sep)
      {
         print_iterable(t, sep);
         if (I != index_length - 1) {
            std::cout << sep;
         }
      }

      // Print a tuple
      template<typename TupType, size_t... I>
      void print_iterable(const TupType& tup, std::index_sequence<I...> index_s, const std::string& sep)
      {
         std::cout << "(";
         (..., print_with_delim(std::get<I>(tup), I, index_s.size(), sep)); // Fold expression: get<0>, get<1>, ...
         std::cout << ")";
      }

      // Wrapper for printing a tuple
      template<typename... T>
      void print_iterable(const std::tuple<T...>& tup, const std::string& sep)
      {
         print_iterable(tup, std::make_index_sequence<sizeof...(T)>(), sep); // make_index_sequence -> 0, 1, 2, ..., N-1 
      }

      // Print a pair
      template<class T1, class T2>
      void print_iterable(const std::pair<T1, T2>& pair, const std::string& sep)
      {
         std::cout << "(";
         print_iterable(pair.first, sep);
         std::cout << " : ";
         print_iterable(pair.second, sep); 
         std::cout << ")";
      }

      // Print an iterable (can be deeply nested)
      template<Iterable T>
      void print_iterable(const T& t, const std::string& sep)
      {  
         int indx = 0;
         std::cout << '[';
         for (auto it = t.begin(); it != t.end(); ++it) {
            print_iterable(*it, sep); // <---- Recursive call to go one level deeper

            if (indx++ != t.size() - 1) { 
               std::cout << sep; 
            }
         } 
         std::cout << ']';
      }

   } // -> end of private namespace


   // Wrapper
   template<typename T>
   void print(const T& t, const std::string& sep, const std::string& end)
   {
      print_iterable(t, sep);
      std::cout << end;
   }
} // -> end on print namespace
```
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Here is a library (I wrote) that will serialize any C++ structure into JSON. github.com/Loki-Astari/ThorsSerializer There is a header only version. It serializes classes (with only an extra declaration) and all the standard containers are handeled automatically. \$\endgroup\$ Dec 1, 2023 at 23:01

1 Answer 1

4
\$\begingroup\$

C++23 and the {fmt} library can print ranges

I was mad C++ did not have support for printing containers.

C++23 will have the ability to print the contents of containers, and even better of any range. There are some notable differences between how std::print() will format ranges and how your function works. In particular, std::print() will allow you to specify how to format the individual elements of a range using a format string. Customizing the start, end and separator characters might be possible by implementing your own formatter for the types you want to print.

I also could not find a header only library for printing containers so I decided to make my own.

std::format() and std::print() are based on the {fmt} library. According to the documentation, you can configure it to be a header-only library.

Consider writing a custom formatter for standard library functions instead

The problem with your function is that it doesn't work like existing formatting functions in the standard library. However, you can write your own formatters that integrate with those. For example, you can write an operator<<(std::ostream&, …) overload for ranges so you can write:

std::vector<int> foo{1, 2, 3};
std::cout << foo << '\n';

Similarly, since C++20 you can specialize std::formatter to do something similar for std::format() and friends, so you could write:

std::cout << std::format("{}\n", foo);

You can still make your custom formatter a header-only library, you can still use templates so it will be able to print most common types, and it's even easier because users don't have to learn how to use a new printing function.

One benefit of a custom formatter over a print() function is that you can print to things other than std::cout, for example to a file or to a stringstream.

Another benefit is that it now works in exactly the same way as formatting standard types. That means you can write a generic function:

template<typename T>
print_key_value(const std::string& key, const T& value) {
    std::cout << std::format("{}: {}\n", key, value);
}

And it will be able to print both containers and simple types, without needing to treat T being a container as a special case.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ Thank you for your feedback. What do you think about the general style of the code? And also I do not quite understand the benefits of formatting rather than printing. I only have one public function which is print. Is it not easier to just say print(this) rather than std::cout << format(this). \$\endgroup\$
    – Doruk
    Dec 1, 2023 at 8:18
  • \$\begingroup\$ @Doruk: The std::cout << format(this) is more C++ like and allows you to change std::cout for any stream like class that may be memory or even a socket. \$\endgroup\$ Dec 1, 2023 at 23:03
  • 1
    \$\begingroup\$ C++23 is supposed to add std::print and std::println with first argument of std::format_string, just like std::format. They work with std::FILE as well as std::istream and std::fstream. Investing on std::formatter for user types is the winning strategy. \$\endgroup\$
    – Red.Wave
    Dec 9, 2023 at 9:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.