6
\$\begingroup\$

I am learning C++ and currently playing with variadic template functions and formatting string, So here is my small attempt to write a formatting function and provide a simple iostream-like interface for std::FILE, I am using '%' as a format specifier f.e

format("const CharT *str") == "const CharT *str"
format("HelloWorld - %", 10) == "HelloWorld - 10"

here is the code:

#include <cstdlib>
#include <string>

namespace gupta {
using string_t = std::string;
using CharT = string_t::value_type;

inline string_t format(const CharT *str) { return str; }
template <typename T> inline string_t to_string(const T &f) { return std::to_string(f); }
inline string_t to_string(const char *str) { return std::string{str}; }
inline string_t to_string(string_t s) { return (s); }

template <typename T, typename... Ts>
string_t format(const CharT *str, const T &arg, const Ts &... args) {
  string_t res;
  for(; *str; str++) {
    if(*str == '%') {
      if(*(str + 1) == '%') {
        str++;
      }
      else {
        res += to_string(arg);
        return res += format(str + 1, args...);
      }
    }
    res += *str;
  }
  return res;
}

template <typename... Ts> auto fprint(std::FILE *f, const char *str, Ts &&... args) {
  auto s = std::move(format(str, std::forward<Ts>(args)...));
  return fwrite(s.data(), 1, s.size(), f);
}

template <typename... Ts> inline auto print(const char *str, Ts &&... args) {
  return fprint(stdout, str, std::forward<Ts>(args)...);
}

template <typename... Ts> inline auto debug(const char *str, Ts &&... args) {
  return fprint(stderr, str, std::forward<Ts>(args)...);
}

namespace detail {
  class _stdout_object {};
  class _stderr_object {};
} // namespace detail

namespace printing_shortcuts {

  template <typename T> detail::_stdout_object operator<<(detail::_stdout_object f, const T &arg) {
    auto s = to_string(arg);
    fwrite(s.data(), 1, s.size(), stdout);
    return f;
  }

  template <typename T> detail::_stderr_object operator<<(detail::_stderr_object f, const T &arg) {
    auto s = to_string(arg);
    fwrite(s.data(), 1, s.size(), stderr);
    return f;
  }

  detail::_stdout_object print() { return {}; }
  detail::_stderr_object debug() { return {}; }

} // namespace printing_shortcuts

using namespace printing_shortcuts;

} // namespace gupta

using namespace gupta::printing_shortcuts;

class test {};

std::string to_string(const test &) { return "test"; }

#include <assert.h>
int main() {
  using namespace gupta;
  assert(format("const CharT *str") == "const CharT *str");
  assert(format("HelloWorld - %", 10) == "HelloWorld - 10");
  print("%s\n", format("HelloWorld - %,%", 10, test{}));
  assert(format("HelloWorld - %,%", 10, test{}) == "HelloWorld - 10,test");
}

also, does it make sense to replace class _stdout_object while their pointers with something like

namespace detail {
  class _stdout_object;
  class _stderr_object;
} // namespace detail

namespace printing_shortcuts {

  template <typename T> detail::_stdout_object *operator<<(detail::_stdout_object *, const T &arg) {
    auto s = to_string(arg);
    fwrite(s.data(), 1, s.size(), stdout);
    return nullptr;
  }

  template <typename T> detail::_stderr_object *operator<<(detail::_stderr_object *, const T &arg) {
    auto s = to_string(arg);
    fwrite(s.data(), 1, s.size(), stderr);
    return nullptr;
  }

  detail::_stdout_object *print() { return nullptr; }
  detail::_stderr_object *debug() { return nullptr; }

} // namespace printing_shortcuts
\$\endgroup\$
7
  • \$\begingroup\$ Last part of your question sounds hard to answer without additional context. Is there a specific reason you want pointers? \$\endgroup\$ Commented Jul 24, 2018 at 14:23
  • \$\begingroup\$ just wondering around, maybe pointers will save some copying \$\endgroup\$
    – bluedragon
    Commented Jul 24, 2018 at 14:24
  • \$\begingroup\$ I recommend waiting a day until accepting an answer. There might be more stellar answers, or existing ones could change a lot. Those don't happen all the time, but they surely do. \$\endgroup\$ Commented Jul 24, 2018 at 16:20
  • \$\begingroup\$ You may find this useful. codereview.stackexchange.com/q/188812/507 \$\endgroup\$ Commented Jul 24, 2018 at 17:50
  • \$\begingroup\$ Here is a set of tests to compare your implementation against the printf() implementation. github.com/Loki-Astari/ThorsIOUtil/blob/master/src/ThorsIOUtil/… \$\endgroup\$ Commented Jul 24, 2018 at 17:51

2 Answers 2

5
\$\begingroup\$

Don't make unnecessary type aliases

using string_t = std::string;
using CharT = string_t::value_type;

Those neither add readability nor make the existing facilities easy to use. When seeing those, I always recall the matlab C++ code generator, and the code it generated was frankly rubbish (Your code is good, it's just that those aliases trigger the feeling of disgust).

Take by std::string_view

I know you tagged this as C++14, but support for C++17 language is already good on all compilers, and standard library implementations are catching up. std::string_view is just more explicit way of saying that you don't need a copy of a string, just wanna read it.

Less verbose ways to overload

I'd be inclined to use if constexpr or tag based dispatch. The latter would probably end up the same length as current code.

Don't inhibit RVO

auto s = std::move(format(str, std::forward<Ts>(args)...));

RVO/NRVO (named return value optimization) is an optimization performed by compiler which is a form of copy elision. Compilers are allowed to eliminate a copy in some cases (can't recall strictly in which cases) Move would happen anyway, and in C++17 the guaranteed copy elision would work anyway, too.

Easy to use correctly, hard to use incorrectly

Perhaps I read too much of "Nobody wants to read your sh*t", but I see some similarity between what Scott Meyers writes and what Steven Pressfield describes. Both try to convey idea of being user/reader-friendly. Guiding them at the correct usage/idea, rather than expecting them to know it (or at least to try one's best). At the moment, the code doesn't throw exception if number of arguments is not equal to number of replacement symbols in the format string, nor any other warning/error. Not everybody knows that the functions don't support custom types with operator<< overloaded. C++ users usually use std::fstream, or some other custom build streams, which the code doesn't support. Very few C++ programmers are paranoid about ADL, which the code will invoke in case the intended print didn't compile. Some ADL calls are extremely evil.

People praise code for being nice, and great interface and reasonable performance is sufficient definition of nice (IMO). Implementation can be amended as time goes by, but changing interface/contracts is rarely an option. Although implementation matters, having great interface offers much greater possibilities of optimizations.

Executable size is not that important

Size of iostream gets dwarfed by the rest of application, usually. Using standard library algorithms, or especially boost, will make additional size of iostream insignificant. It might be important in some cases, but those who encounter would usually have no std::FILE either, as they want to output to embedded 8*8 LED screen or something like that.

Alternative implementation

I would use pseudo runtime tuple indexing to access elements, rather than recursing. That would eliminate the weird edge case of no arguments, and make everything more consistent.

Important excerpt:

template <std::size_t static_index, typename ... ArgumentTypes>
std::string runtime_get(std::size_t runtime_index, std::tuple<ArgumentTypes...>&& rest) {
    if constexpr (static_index == std::size_t(-1)) {
        throw std::invalid_argument("number of format symbols is more than arguments");
    }
    else {
        if (runtime_index == static_index)
            return to_string(std::get<static_index>(rest));
        else
            return to_string(runtime_get<static_index - 1>(runtime_index, std::move(rest)));
    }
}

template <typename... Ts>
string_t format(const CharT *str, const Ts &... args) {
    std::size_t current_index = 0;
    string_t res;
    for (; *str; str++) {
        if (*str != '%') {
            res += *str;
            continue;
        }
        if (*(str + 1) == '%') {
            res += '%';
            ++str;
        }
        else {
            res += runtime_get<sizeof...(Ts) - 1>(current_index,
                std::forward_as_tuple(args...));
            ++current_index;
        }
    }
    return res;
}

It is easy to see that one can only pass data into a function, but trying to get the wanted element is impossible due to type being different depending on runtime index. The function usually compiles into a loop for differing user defined types, and into jump table in case of the same or only built in types.


Full code of alternative implementation:

#include <cstdlib>
#include <string>
#include <stdexcept>
#include <tuple>

namespace gupta {
    using string_t = std::string;
    using CharT = string_t::value_type;

    template <typename T> inline string_t to_string(const T &f) { return std::to_string(f); }
    inline string_t to_string(const char *str) { return std::string{ str }; }
    inline string_t to_string(string_t s) { return (s); }

    template <std::size_t static_index, typename ... ArgumentTypes>
    std::string runtime_get(std::size_t runtime_index, std::tuple<ArgumentTypes...>&& rest) {
        if constexpr (static_index == std::size_t(-1)) {
            throw std::invalid_argument("number of format symbols is more than arguments");
        }
        else {
            if (runtime_index == static_index)
                return to_string(std::get<static_index>(rest));
            else
                return to_string(runtime_get<static_index - 1>(runtime_index, std::move(rest)));
        }
    }

    template <typename... Ts>
    string_t format(const CharT *str, const Ts &... args) {
        std::size_t current_index = 0;
        string_t res;
        for (; *str; str++) {
            if (*str != '%') {
                res += *str;
                continue;
            }
            if (*(str + 1) == '%') {
                res += '%';
                ++str;
            }
            else {
                res += runtime_get<sizeof...(Ts) - 1>(current_index,
                    std::forward_as_tuple(args...));
                ++current_index;
            }
        }
        return res;
    }

    template <typename... Ts> auto fprint(std::FILE *f, const char *str, Ts &&... args) {
        auto s = std::move(format(str, std::forward<Ts>(args)...));
        return fwrite(s.data(), 1, s.size(), f);
    }

    template <typename... Ts> inline auto print(const char *str, Ts &&... args) {
        return fprint(stdout, str, std::forward<Ts>(args)...);
    }

    template <typename... Ts> inline auto debug(const char *str, Ts &&... args) {
        return fprint(stderr, str, std::forward<Ts>(args)...);
    }

    namespace detail {
        class _stdout_object {};
        class _stderr_object {};
    } // namespace detail

    namespace printing_shortcuts {

        template <typename T> detail::_stdout_object operator<<(detail::_stdout_object f, const T &arg) {
            auto s = to_string(arg);
            fwrite(s.data(), 1, s.size(), stdout);
            return f;
        }

        template <typename T> detail::_stderr_object operator<<(detail::_stderr_object f, const T &arg) {
            auto s = to_string(arg);
            fwrite(s.data(), 1, s.size(), stderr);
            return f;
        }

        detail::_stdout_object print() { return {}; }
        detail::_stderr_object debug() { return {}; }

    } // namespace printing_shortcuts

    using namespace printing_shortcuts;

} // namespace gupta

using namespace gupta::printing_shortcuts;

class test {};

std::string to_string(const test &) { return "test"; }

#include <assert.h>
#include <iostream>

int main() {
    using namespace gupta;
    std::cout << format("%% %", 12) << '\n';
    assert(format("%% %", 12) == "% 12");
}

Demo on Wandbox

\$\endgroup\$
4
  • \$\begingroup\$ For those who wonder why I mentioned "Nobody wants to read your sh*t", I'm writing a book in literary fiction genre (the one about emotions and stuff, no scifi). \$\endgroup\$ Commented Jul 24, 2018 at 15:10
  • \$\begingroup\$ "Should increment twice and also replace two % with one %" - that's exactly what i want to do, double "%%" will print single '%'..... thanks for your advices \$\endgroup\$
    – bluedragon
    Commented Jul 24, 2018 at 15:10
  • \$\begingroup\$ @bluedragon, format("%%") is also a big confusing. I would expect %, but it returns the string itself. I deleted the part about bug, as after some more testing it turns out to be correct. It's just that format with no arguments got me off the tracks. \$\endgroup\$ Commented Jul 24, 2018 at 15:12
  • \$\begingroup\$ @bluedragon, I added a technique which you might be interested in. If you need C++14 version, just use std::integral_constant as argument. \$\endgroup\$ Commented Jul 24, 2018 at 15:53
3
\$\begingroup\$

I don't think there's a good reason for renaming std::string like this:

using string_t = std::string;

It just serves to make the code harder to read (because I have to remember this alias). Just use std::string as is. Similarly, use char where appropriate (as it is, format() and fprint() aren't consistent in this respect.


This function:

template <typename T> inline string_t to_string(const T &f) { return std::to_string(f); }

catches all T not otherwise overloaded. I'd be inclined to

using std::to_string;

That then leaves us free to write a catch-all template, something like

template <typename T>
std::string to_string(const T& val)
{
    std::ostringstream s;
    s << val;
    return s.str();
}

The advantage to this is that many class authors will have written operator<<(std::ostream&, T); already, which saves you bullying them to also write a to_string(T).

We could constrain this template (with Concepts, or with std::enable_if, or with an anonymous template argument).


Exercise: Extend the code to work with other kinds of string (e.g. std::wstring).

\$\endgroup\$
5
  • \$\begingroup\$ I don't like iostream, just including the header doubles my executable size on mingw \$\endgroup\$
    – bluedragon
    Commented Jul 24, 2018 at 14:28
  • \$\begingroup\$ `using std::to_string;' can adl still be used \$\endgroup\$
    – bluedragon
    Commented Jul 24, 2018 at 14:29
  • \$\begingroup\$ Re exercise: Wouldn't changing string_t to using string_t = std::wstring; accomplish that? ^^ \$\endgroup\$
    – hoffmale
    Commented Jul 24, 2018 at 14:47
  • \$\begingroup\$ No, because then it would still only work for one kind of string. I meant that it should work with a std::basic_string<T> for any T. IOW, can the code be templated on the character type? \$\endgroup\$ Commented Jul 24, 2018 at 14:49
  • \$\begingroup\$ @TobySpeight, I'd put SFINAE on the ones that are common, and force the user to provide conversions for the rest (not hoffmale). \$\endgroup\$ Commented Jul 24, 2018 at 14:53

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.