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 mainprint
method uses internally in addition to ausing
declaration. This isn't ideal because I consider the content of this namespace to be an implementation detail of theprint
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 inprivprint
intoprint
'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 ofconstexpr
. This point is why a template-meta-programming tag is on this question.
2 Answers 2
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");
}
-
\$\begingroup\$ can you explain this std::declval<std::ostream>() << std::declval<T>() ? Also, can you make it c++11 compliant? \$\endgroup\$noman pouigt– noman pouigt2019年08月06日 19:31:50 +00:00Commented Aug 6, 2019 at 19:31
-
\$\begingroup\$ @nomanpouigt
declval<T>()
is an function with return typeT&&
. It is used in unevaluated operands (e.g., insidedecltype
) to create an object of typeT
without requiring thatT
has a constructor. See stackoverflow.com/q/28532781. \$\endgroup\$L. F.– L. F.2019年08月06日 23:48:09 +00:00Commented 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.– L. F.2019年08月06日 23:55:07 +00:00Commented Aug 6, 2019 at 23:55
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.
-
\$\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\$Deduplicator– Deduplicator2019年08月06日 17:25:41 +00:00Commented 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\$Matthieu M.– Matthieu M.2019年08月06日 17:48:26 +00:00Commented 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\$Deduplicator– Deduplicator2019年08月06日 18:25:26 +00:00Commented 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\$Matthieu M.– Matthieu M.2019年08月06日 18:31:41 +00:00Commented Aug 6, 2019 at 18:31
Explore related questions
See similar questions with these tags.
\
is the escape character, but your code and your unit tests both indicate that/
is the escape character. \$\endgroup\$