Making a print for C++ like it's python (Part 1)

in programming •  7 years ago 

While I generally like programming C++ very much at the moment, writing short experimental snippets to try something out or for a single use can sometimes be more painful in C++ than in other languages.

One of my least favourite things is the way we use input/output from iostreams. While C-Style output is of course possible, it requires either a matching formatting option for your output, or a to_string function, which hopefully can be found via ADL. In my experience that too often creates a printf roughly like this:

printf("%s %s %s %i\n, to_string(a), to_string(b), x, d);

which I consider not very readable.

What I would like to do instead is to use a print(ln) function like this:

print("The value of", key, "is", value);
println(std::cerr, "Errro while loading", configFile, ":" error);

To the regular C++ programmer (who of course, since this is an expert-friendly language, is an expert), this screens variadic template function, so I set out to create a print-function, which can do this.

It is actually pretty straight-forward to get the unrolling using template-metaprogramming in one of it's simpler forms: recursion with a base case. The simplest version of print looks like this:

// recursion
template <typename T, typename ...Args>
std::ostream& __print(std::ostream& os, T&& val, Args&&... args)
{
    return print(os << std::forward<T>(val), std::forward<Args>(args)...);
}
//basecase
template <typename T>
std::ostream& __print(std::ostream& os, T&& val)
{
    return os << std::forward<T>(val);
}

Functional programmers will immediately scream "this is a fold" and they a right about it. This concept is also the essence of the upcoming C++17 fold-expressions. The basic idea is to take the arguments and put them into the current os, then put that os into the next call and recurse down until you only have one argument left, in which case you just output and return. I have not yet considered an error-handling model I want to pursue with this (I think I would prefer atomic lines or at least offer the option), which also happens to make this more readable.

Now unluckily this does not insert any spaces and although that would be easy to add a space on every call, and if you want to insert the newline you still have to do that manually. Because I have many potential functions I might want to write on top of this like join, sprint, etc. I came up with a solution using an abstract base-class and the visitor-pattern instead:

    detail
    {
       class style_t
      {
        public:
          virtual std::ostream& visitSentinel(std::ostream& os) const = 0;
          virtual std::ostream& visitSeparator(std::ostream& os) const = 0;
          virtual std::ostream& visitTerminator(std::ostream& os) const = 0;
      };

      class string_style_t : public style_t
      {
        std::string sentinel_;
        std::string separator_;
        std::string terminator_;
        public:
        string_style_t(
            std::string sentinel, std::string separator, std::string terminator):
          sentinel_(std::move(sentinel)),
          separator_(std::move(separator)),
          terminator_(std::move(terminator))
        {}
        std::ostream& visitSentinel(std::ostream& os) const override
        {
          return os << sentinel_;
        }
        std::ostream& visitSeparator(std::ostream& os) const override
        {
          return os << separator_;
        }
        std::ostream& visitTerminator(std::ostream& os) const override
        {
          return os << terminator_;
        }
      };
    }

The upside of this approach is that by using run-time polymorphism, the implementation of styles becomes easier, and on the interface, it is only commited to std::ostream, which is general yet specific enough to hopefully immediate imply its use to an experienced programmer. Using this and wrapper functions to instantiate the two styles, which I immediately knew I was gonna use, the final product looks like this:

#ifndef MDORNER_PRINT_PRINT_H
#define MDORNER_PRINT_PRINT_H

#include <string>
#include <ostream>

namespace mdorner
{
  namespace print
  {
    namespace detail
    {
      class style_t
      {
        public:
          virtual std::ostream& visitSentinel(std::ostream& os) const = 0;
          virtual std::ostream& visitSeparator(std::ostream& os) const = 0;
          virtual std::ostream& visitTerminator(std::ostream& os) const = 0;
      };

      class string_style_t : public style_t
      {
        std::string sentinel_;
        std::string separator_;
        std::string terminator_;
        public:
        string_style_t(
            std::string sentinel, std::string separator, std::string terminator):
          sentinel_(std::move(sentinel)),
          separator_(std::move(separator)),
          terminator_(std::move(terminator))
        {}
        std::ostream& visitSentinel(std::ostream& os) const override
        {
          return os << sentinel_;
        }
        std::ostream& visitSeparator(std::ostream& os) const override
        {
          return os << separator_;
        }
        std::ostream& visitTerminator(std::ostream& os) const override
        {
          return os << terminator_;
        }
      };

      template <typename T, typename ...Args>
        std::ostream& __print(const style_t& style, std::ostream& os, T&& val, Args&&... args)
        {
          return __print(style, style.visitSeparator(os << std::forward<T>(val)), std::forward<Args>(args)...);
        }

      template <typename T>
        std::ostream& __print(const style_t& style, std::ostream& os, T&& val)
        {
          return style.visitTerminator(os << std::forward<T>(val));
        }
    } // end detail namespace

    template <typename ...Args>
      void print(std::ostream& os, Args&&... args)
      {
        const auto style = detail::string_style_t("", " ", "");
        detail::__print(style, style.visitSentinel(os), std::forward<Args>(args)...);
      }

    template <typename ...Args>
      void println(std::ostream& os, Args&&... args)
      {
        const auto style = detail::string_style_t("", " ", "\n");
        detail::__print(style, style.visitSentinel(os), std::forward<Args>(args)...);
      }
  }
}

#endif

This actually allows you to print anything implementing operator << like this:

println(std::cout, "Some text", some_int, some_user_defined_class);

I am currently working out the ways in which this can be used before I decide on how to move on, and I have started setting up basic testing with Catch, which will hopefully allow me to consider what style of error-handling makes most sense.

This current version of this code is on github: https://github.com/mdorner/cpprint

I am happy to hear comments on my approach, and if you have tried something similar.

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Congratulations @midor! You have completed some achievement on Steemit and have been rewarded with new badge(s) :

You made your First Vote

Click on any badge to view your own Board of Honor on SteemitBoard.
For more information about SteemitBoard, click here

If you no longer want to receive notifications, reply to this comment with the word STOP

By upvoting this notification, you can help all Steemit users. Learn how here!

Congratulations @midor! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 2 years!

You can view your badges on your Steem Board and compare to others on the Steem Ranking

Vote for @Steemitboard as a witness to get one more award and increased upvotes!