I have a compact set of functions that I use to write arbitrary data to a comma separated values file. It looks something like this:
template<typename T>
void log(std::ostream& out, T a) {
out << a << std::endl;
}
template<typename T, typename... Args>
void log(std::ostream& out, T a, Args... args) {
out << a << ",";
log(out, args...);
}
int main()
{
log(std::cout, 1, "a");
}
Which outputs what I would expect:
1,a
However, when writing strings that have embedded commas the values should be wrapped in quotes. So I want:
log(1, "a, b");
To output:
1,"a, b"
Not:
1,a, b
I tried to address this by adding quoting functions that look like this:
/* For any old type, just write it out. */
template<typename T>
void quote(std::ostream& out, T a) {
out << a;
}
/* Specialize for std::string. */
template<>
void quote<std::string>(std::ostream& out, std::string a) {
if (a.find(',') == std::string::npos) {
out << a;
}
else {
out << '"' << a << '"';
}
}
/* Log a single value. */
template<typename T>
void log(std::ostream& out, T a) {
quote(out, a);
out << std::endl;
}
/** Log multiple values. */
template<typename T, typename... Args>
void log(std::ostream& out, T a, Args... args) {
quote(out, a);
out << ",";
Log(out, args...);
}
If I log the data as an lvalue this works.
std::string text {"a, b"};
log(std::cout, 1, text);
Produces the expected output:
1,"a, b"
But passing rvalues does not seem to work.
I tried specializing the quote method for std::string& std::string&& std::string const&, and std::string const&&, but nothing seems to work.
Below is the full set of things I tried:
// Example program
#include <iostream>
#include <string>
/* For any old type, just write it out. */
template<typename T>
void quote(std::ostream& out, T a) {
out << a;
}
/* Output a string wrapped in "'s if it contains a comma. */
template<>
void quote<std::string>(std::ostream& out, std::string a) {
if (a.find(',') == std::string::npos) {
out << a;
}
else {
out << '"' << a << '"';
}
}
/* Output a string wrapped in "'s if it contains a comma. */
template<>
void quote<std::string const>(std::ostream& out, std::string const a) {
if (a.find(',') == std::string::npos) {
out << a;
}
else {
out << '"' << a << '"';
}
}
/* Output a string wrapped in "'s if it contains a comma. */
template<>
void quote<std::string const&>(std::ostream& out, std::string const& a) {
if (a.find(',') == std::string::npos) {
out << a;
}
else {
out << '"' << a << '"';
}
}
/* Output a string wrapped in "'s if it contains a comma. */
template<>
void quote<std::string const&&>(std::ostream& out, std::string const&& a) {
if (a.find(',') == std::string::npos) {
out << a;
}
else {
out << '"' << a << '"';
}
}
/* log a single value. */
template<typename T>
void log(std::ostream& out, T a) {
quote(out, a);
out << std::endl;
}
/** log multiple values. */
template<typename T, typename... Args>
void log(std::ostream& out, T a, Args... args) {
quote(out, a);
out << ",";
log(out, args...);
}
int main()
{
std::string b { "a, b" };
log(std::cout, 1, b);
log(std::cout, 1, "c, d");
}
Which outputs:
1,"a, b"
1,c, d
Note: For the project I'm working on we're limited to C++11 and boost is not available.
The reason your specializations don't work for a value such as "c, d"
is because its type is char const*
, not std::string
. The main quote
template will be called by default unless the argument type exactly matches one of the specializations. Your example with std::string b
works because of the std::string
specialization (and not for example because of the std::string const&
one).
You could add more specializations, like this for example:
template<>
void quote<char const*>(std::ostream& out, char const* a) {
if (std::string(a).find(',') == std::string::npos) {
out << a;
}
else {
out << '"' << a << '"';
}
}
However, I don't think this solves your problem. What you actually want is to handle any case where the argument is convertible to a std::string
.
Also, you probably want to take the arguments by const reference.
You can write the whole thing in one function, provided you are using C++17 (for if constexpr
):
template<typename T, typename... Args>
void log(std::ostream& out, T const& a, Args const&... args) {
if constexpr (std::is_convertible_v<T, std::string>) {
if (static_cast<std::string const&>(a).find(',') != std::string::npos) {
out << '"' << a << '"';
} else {
out << a;
}
} else {
out << a;
}
if constexpr (sizeof...(args) > 0) {
out << ",";
log(out, args...);
} else {
out << std::endl;
}
}
Here is a slightly more complex version using a fold expression instead of recursion:
template<typename T, typename... Args>
void log(std::ostream& out, T const& a, Args const&... args) {
auto quote = [&out](auto const& a) {
if constexpr (std::is_convertible_v<decltype(a), std::string>) {
if (static_cast<std::string const&>(a).find(',') != std::string::npos) {
out << '"' << a << '"';
} else {
out << a;
}
} else {
out << a;
}
};
quote(a);
((out << ',', quote(args)), ...);
out << std::endl;
}
Prior to C++17, the same thing can be achieved with tag dispatching:
template<typename T>
void quote(std::ostream& out, T const& a, std::true_type) {
if (static_cast<std::string const&>(a).find(',') == std::string::npos) {
out << a;
} else {
out << '"' << a << '"';
}
}
template<typename T>
void quote(std::ostream& out, T const& a, std::false_type) {
out << a;
}
template<typename T>
void log(std::ostream& out, T const& a) {
quote(out, a, std::is_convertible<T, std::string>{});
out << std::endl;
}
template<typename T, typename... Args>
void log(std::ostream& out, T const& a, Args&&... args) {
quote(out, a, std::is_convertible<T, std::string>{});
out << ",";
log(out, std::forward<Args>(args)...);
}