c++templatesrvalue

Template specialization for rvalues


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.


Solution

  • 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;
        }
    }
    

    Demo

    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;
    }
    

    Demo

    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)...);
    }
    

    Demo