10
\$\begingroup\$

I am currently working through learning C++'s variadic templates and I wanted to create an example use of them that is less trivial than the one in my book. I decided to create a modification of printf's interface where the type of each argument does not need to be specified. Here is an example:

print(std::cout, "Welcome to this computer program, % son of %!", "bob", "some klingon");
//prints "Welcome to this computer program, bob son of some klingon."

I have also added the ability to escape a % character (or any other character, like the escape character itself) with a /.

#include <iostream>
#include <string>
#include <ostream>

namespace printpriv{
    using itr = std::string::const_iterator;

    template<typename T>
    void print(std::ostream &out, itr it, itr end, const T &t){
        bool flagged = false;
        while(it != end){
            if(flagged){
                out << *it;
                flagged = false;
            }else{
                if(*it == '/'){
                    flagged = true;
                }else if(*it=='%') {
                    out << t;
                }else{
                    out << *it;
                }
            }
            ++it;
        }
    }

    template<typename T, typename ... Args>
    void print(std::ostream &out, itr it, itr end, const T &t, const Args &...args){
        bool flagged = false;
        while(it != end){
            if(flagged){
                out << *it;
                flagged = false;
            }else{
                if(*it == '/'){
                    flagged = true;
                }else if(*it=='%') {
                    out<<t;
                    print(out, ++it, end, args...);
                    break;
                }else{
                    out << *it;
                }
            }
            ++it;
        }
    }
}

//to handle case of no arguments
void print(std::ostream &out, const std::string &pattern){
    out << pattern;
}

template<typename ... Args>
void print(std::ostream &out, const std::string &pattern, const Args &...args){
    printpriv::print(out, pattern.begin(), pattern.end(), args...);
}

int main() {
    print(std::cout, "Hello, I am a % and my name is %.\n", "dog", "capybara");
    print(std::cout, "There are % % here.\n", 32, std::string("capybaras"));
    print(std::cout, "There are no arguments here.\n");
    print(std::cout, "I am 80/% sure that this will work.", 10); //at least one argument is needed to prevent calling the version that just passes through the pattern to the output stream
    return 0;
}

In this code, I have these primary concerns but I am also interested in a standard code review:

  • There is a huge amount of code duplication between the two privprint::print overloads to handle parsing the input pattern, bur I don't see any good way to avoid it.
  • The privprint namespace stores two functions that the main print method uses internally in addition to a using declaration. This isn't ideal because I consider the content of this namespace to be an implementation detail of the print function, so it should not be accessible to the user. Adding "priv" in the name is a decent way to warn other programmers who may work with this code, but something that the compiler enforces would be ideal. My instinct is to put the using declaration and the function templates that are in privprint into print's namespace, but C++ does not allow function templates to be in a function's namespace (nor does it seem to allow any templates at all, which breaks all of my usual ways of getting around this function restriction, like a functor type or a lambda).
  • The implementation here considers a mis-matched number of un-escaped %s and arguments to substitute to be undefined behavior, but it would be better if it were a compile-time error when the string is available. I could make it a run-time error, but that would add run-time cost and thus violate C++'s philosophy of preferring undefined behavior to increased run-time cost. Despite this, I don't see any good way of allowing the templates to parse the string at run time. I suspect that this is possible however by making some use of constexpr. This point is why a template-meta-programming tag is on this question.
\$\endgroup\$
8
  • \$\begingroup\$ Your English documentation says that \ is the escape character, but your code and your unit tests both indicate that / is the escape character. \$\endgroup\$ Commented Aug 6, 2019 at 4:20
  • \$\begingroup\$ @Quuxplusone Oops. It's fixed now. \$\endgroup\$
    – john01dav
    Commented Aug 6, 2019 at 4:22
  • 4
    \$\begingroup\$ Do you know of the fmt library that is likely to be standardized soon? \$\endgroup\$
    – L. F.
    Commented Aug 6, 2019 at 4:53
  • \$\begingroup\$ @Incomputable: It did, actually => reddit.com/r/cpp/comments/cfk9de/… (though in a lightweight version). \$\endgroup\$ Commented Aug 6, 2019 at 14:24
  • \$\begingroup\$ @MatthieuM., thanks for sharing the link, lots of useful info there. \$\endgroup\$ Commented Aug 6, 2019 at 18:36

2 Answers 2

8
\$\begingroup\$

General design

Currently, your function is defined to have "undefined behavior" if the number of arguments is wrong. This is sub-optimal. Checking is trivial in this case, so report the problem in some way instead of producing strange output.

I could make it a run-time error, but that would add run-time cost and thus violate C++'s philosophy of preferring undefined behavior to increased run-time cost.

No, in this case run-time checking incurs zero overload on valid input. Reporting the error on invalid input is much more efficient than outputting strange things.

You are handling empty template parameter packs specially. This is unnecessary. And this makes calls like print("80/%") produce the wrong result.

It is advised to put your utilities in your own namespace with a unique name, and put the non-exposed parts in a nested detail namespace. (In C++20, we will be able to have fine-grained control over what to expose in a module.)

print should take a std::string_view instead of const std::string& to avoid unnecessary allocation. It would also be nice if the function is constrained to be SFINAE-friendly.

Also, it would be nice if you make this into a I/O manipulator, so that it can be used like

std::cout << print("% * 80/% = %", 5, 4) << '\n';

Code

Your code seems to be very conservative on the usage of spaces around braces. It will look nice if they don't squeeze together:

if (condition) {
    // ...
} else {
    // ...
}

while (condition) {
    // ...
}

The print code only needs <ostream>, not <iostream>.

Your handling of escape sequences is a bit convoluted. A stray / at the end of the sequence should be invalid, not simply ignored.


Here's my extended code:

// library

#include <cassert>
#include <ostream>
#include <string_view>
#include <tuple>
#include <type_traits>

// replace unicorn304 with your namespace
namespace unicorn304::detail {
    template <typename It>
    void print(std::ostream& os, It first, It last)
    {
        for (auto it = first; it != last; ++it) {
            switch (*it) {
            case '%':
                throw std::invalid_argument{"too few arguments"};
            case '/':
                ++it;
                if (it == last)
                    throw std::invalid_argument{"stray '/'"};
                [[fallthrough]];
            default:
                os << *it;
            }
        }
    }

    template <typename It, typename T, typename... Args>
    void print(std::ostream& os, It first, It last,
               const T& arg, const Args&... args)
    {
        for (auto it = first; it != last; ++it) {
            switch (*it) {
            case '%':
                os << arg;
                return print(os, ++it, last, args...);
            case '/':
                ++it;
                if (it == last)
                    throw std::invalid_argument{"stray '/'"};
                [[fallthrough]];
            default:
                os << *it;
            }
        }
        throw std::invalid_argument{"too many arguments"};
    }

    template <typename... Args>
    struct Printer {
        std::string_view format;
        std::tuple<const Args&...> args;
    };

    template <typename... Args, std::size_t... Is>
    void printer_helper(std::ostream& os, const Printer<Args...>& printer,
                        std::index_sequence<Is...>)
    {
        print(os, printer.format.begin(), printer.format.end(),
              std::get<Is>(printer.args)...);
    }

    template <typename... Args>
    std::ostream& operator<<(std::ostream& os, const Printer<Args...>& printer)
    {
        printer_helper(os, printer, std::index_sequence_for<Args...>{});
        return os;
    }
}

namespace unicorn304 {
    template <typename T, typename = void>
    struct is_ostreamable :std::false_type {};
    template <typename T>
    struct is_ostreamable<T, std::void_t<
        decltype(std::declval<std::ostream>() << std::declval<T>())>
    > :std::true_type {};
    template <typename T>
    inline constexpr bool is_ostreamable_v = is_ostreamable<T>::value;

    template <typename... Args,
              std::enable_if_t<std::conjunction_v<is_ostreamable<Args>...>, int> = 0>
    auto print(std::string_view format, const Args&... args)
    {
        return detail::Printer<Args...>{format, std::forward_as_tuple(args...)};
    }
}

Example usage:

void print_test()
{
    using unicorn304::print;

    std::cout << print("% * 80/% = %\n", 5, 4)
              << print("% son of %!\n", "bob", "some klingon")
              << print("slash is '//' percent is '/%'\n");
}
\$\endgroup\$
3
  • \$\begingroup\$ can you explain this std::declval<std::ostream>() << std::declval<T>() ? Also, can you make it c++11 compliant? \$\endgroup\$ Commented Aug 6, 2019 at 19:31
  • \$\begingroup\$ @nomanpouigt declval<T>() is an function with return type T&&. It is used in unevaluated operands (e.g., inside decltype) to create an object of type T without requiring that T has a constructor. See stackoverflow.com/q/28532781. \$\endgroup\$
    – L. F.
    Commented Aug 6, 2019 at 23:48
  • \$\begingroup\$ @nomanpouigt As for C++11, well, I didn't really think of that. That would make things a bit complicated, but in theory it should be doable. I don't feel like doing that now, though. \$\endgroup\$
    – L. F.
    Commented Aug 6, 2019 at 23:55
2
\$\begingroup\$

Shims are wonderful

I recommend using a shim to offload all the logic to a function manipulating a list (rather than a pack) of arguments. As a bonus, you'll also be able to push the bulk of the implementation into a .cpp file.

Essentially, your goal is to invoke:

namespace details {

class Argument {
public:
    virtual void print(std::ostream& out) const = 0;
protected:
    ~Argument() = default;
};

void print_impl_inner(
    std::ostream& out,
    std::string_view format,
    std::span<Argument const*> arguments
);

This is done by creating a shim for each argument:

template <typename T>
class ArgumentT final : public Argument {
public:
    explicit ArgumentT(T const& t): mData(t) {}

    void print(std::ostream& out) const final { out << mData; }

private:
    T const& mData;
};

template <typename T>
ArgumentT<T> make_argument(T const& t) { return ArgumentT<T>(t); }

And then automating the creation and passing of the shims:

template <typename... Args>
void print_impl_outer(
    std::ostream& out,
    std::string_view format,
    Args const&... args
)
{
    Arguments const* const array[sizeof...(args)] =
        { static_cast<Argument const*>(&args)... };
    print_impl_inner(out, format, array);
}

} // namespace details

template <typename... Args>
void print(
    std::ostream& out,
    std::string_view format,
    Args&&... args
)
{
    details::print_impl_outer(out, format,
        details::make_argument(std::forward<Args>(args))...);
}

Thus the user interface is this variadic template print function, however the actual implementation is done in details::print_impl_inner, which is only declared in the header.

Then, in a .cpp file:

void details::print_impl_inner(
    std::ostream& out,
    std::string_view format,
    std::span<Argument const*> arguments
)
{
    std::size_t a = 0;

    for (std::size_t i = 0, max = format.size(); i != max; ++i) {
        switch (format[i]) {
        case '%':
            if (a == arguments.size()) {
                throw std::invalid_argument{"Too few arguments"};
            }
            arguments[a]->print(out);
            ++a;
            break;
        case '\\':
            ++i;
            if (i == max) {
                throw std::invalid_argument{
                    "Invalid format string: stray \\ at end of string"};
            }
            [[fallthrough]];
        default:
            os << format[i];
        }
    }
}

Note: if you do not have access to std::span, you can use gsl::span with minor adaptations.

Extensibility

The beauty of this architecture is dual:

  • You can easily improve the format scan pass and printing without any guilt at "polluting" the header.
  • You can easily implement indexed access to the arguments, that is %N meaning print the N-th argument.
  • You can easily specialize the printers for a specific subset of arguments, by adding multiple make_argument overloads returning dedicated printers.

For example, consider implementing a Python-like format language:

print(std::cout, "Hello {1:<12}, I'm {0:x} years old", my_age, your_name);

Where 1 and 0 are the indexes and whatever is right of : is a specific format request (here alignment and width).

With this shim implementation, it should be relatively straightforward, and the header will remain lightweight.

\$\endgroup\$
4
  • \$\begingroup\$ Quite nice. Still, a slightly more complicated implementation, separating the array of formatters (compile-time-constant) from the array of arguments (dynamic) might be better. \$\endgroup\$ Commented Aug 6, 2019 at 17:25
  • \$\begingroup\$ @Deduplicator: I think I see where this is going, essentially have an array of pointer-to-function and an array of void const*, however I am not sure how helpful it would be. \$\endgroup\$ Commented Aug 6, 2019 at 17:48
  • \$\begingroup\$ Well, if there are only two arguments or less, it doesn't matter much at all. If there are more, having them as statically allocated constants reduces the needed work. \$\endgroup\$ Commented Aug 6, 2019 at 18:25
  • 1
    \$\begingroup\$ @Deduplicator: Possibly, though I am not sure it would make a big difference compared to the cost of parsing (the format string) + formatting (the arguments). I'd rather keep it tidy unless proven to be a bottle-neck, and focus on improving the parsing/formatting performance. \$\endgroup\$ Commented Aug 6, 2019 at 18: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.