漂亮的字体 std: : tuple

这是我上一个关于 美化打印 STL 容器的问题的后续,为此我们设法开发了一个非常优雅和完全通用的解决方案。


在下一步中,我将使用可变模板(所以这是严格的 C + + 11)为 std::tuple<Args...>包含美观打印。对于 std::pair<S,T>,我只是说

std::ostream & operator<<(std::ostream & o, const std::pair<S,T> & p)
{
return o << "(" << p.first << ", " << p.second << ")";
}

打印元组的类似结构是什么?

我尝试了各种模板参数堆栈解包,传递索引,并使用 SFINAE 来发现什么时候我在最后一个元素,但没有成功。我不会用我的破代码给您增加负担; 希望问题描述足够直接。基本上,我喜欢以下行为:

auto a = std::make_tuple(5, "Hello", -0.1);
std::cout << a << std::endl; // prints: (5, "Hello", -0.1)

包含与前一个问题相同的通用级别(char/wchar _ t,对分隔符)的额外加分!

34799 次浏览

Yay, indices~

namespace aux{
template<std::size_t...> struct seq{};


template<std::size_t N, std::size_t... Is>
struct gen_seq : gen_seq<N-1, N-1, Is...>{};


template<std::size_t... Is>
struct gen_seq<0, Is...> : seq<Is...>{};


template<class Ch, class Tr, class Tuple, std::size_t... Is>
void print_tuple(std::basic_ostream<Ch,Tr>& os, Tuple const& t, seq<Is...>){
using swallow = int[];
(void)swallow{0, (void(os << (Is == 0? "" : ", ") << std::get<Is>(t)), 0)...};
}
} // aux::


template<class Ch, class Tr, class... Args>
auto operator<<(std::basic_ostream<Ch, Tr>& os, std::tuple<Args...> const& t)
-> std::basic_ostream<Ch, Tr>&
{
os << "(";
aux::print_tuple(os, t, aux::gen_seq<sizeof...(Args)>());
return os << ")";
}

Live example on Ideone.


For the delimiter stuff, just add these partial specializations:

// Delimiters for tuple
template<class... Args>
struct delimiters<std::tuple<Args...>, char> {
static const delimiters_values<char> values;
};


template<class... Args>
const delimiters_values<char> delimiters<std::tuple<Args...>, char>::values = { "(", ", ", ")" };


template<class... Args>
struct delimiters<std::tuple<Args...>, wchar_t> {
static const delimiters_values<wchar_t> values;
};


template<class... Args>
const delimiters_values<wchar_t> delimiters<std::tuple<Args...>, wchar_t>::values = { L"(", L", ", L")" };

and change the operator<< and print_tuple accordingly:

template<class Ch, class Tr, class... Args>
auto operator<<(std::basic_ostream<Ch, Tr>& os, std::tuple<Args...> const& t)
-> std::basic_ostream<Ch, Tr>&
{
typedef std::tuple<Args...> tuple_t;
if(delimiters<tuple_t, Ch>::values.prefix != 0)
os << delimiters<tuple_t,char>::values.prefix;


print_tuple(os, t, aux::gen_seq<sizeof...(Args)>());


if(delimiters<tuple_t, Ch>::values.postfix != 0)
os << delimiters<tuple_t,char>::values.postfix;


return os;
}

And

template<class Ch, class Tr, class Tuple, std::size_t... Is>
void print_tuple(std::basic_ostream<Ch, Tr>& os, Tuple const& t, seq<Is...>){
using swallow = int[];
char const* delim = delimiters<Tuple, Ch>::values.delimiter;
if(!delim) delim = "";
(void)swallow{0, (void(os << (Is == 0? "" : delim) << std::get<Is>(t)), 0)...};
}

I got this working fine in C++11 (gcc 4.7). There are I am sure some pitfalls I have not considered but I think the code is easy to read and and not complicated. The only thing that may be strange is the "guard" struct tuple_printer that ensure that we terminate when the last element is reached. The other strange thing may be sizeof...(Types) that return the number of types in Types type pack. It is used to determine the index of the last element (size...(Types) - 1).

template<typename Type, unsigned N, unsigned Last>
struct tuple_printer {


static void print(std::ostream& out, const Type& value) {
out << std::get<N>(value) << ", ";
tuple_printer<Type, N + 1, Last>::print(out, value);
}
};


template<typename Type, unsigned N>
struct tuple_printer<Type, N, N> {


static void print(std::ostream& out, const Type& value) {
out << std::get<N>(value);
}


};


template<typename... Types>
std::ostream& operator<<(std::ostream& out, const std::tuple<Types...>& value) {
out << "(";
tuple_printer<std::tuple<Types...>, 0, sizeof...(Types) - 1>::print(out, value);
out << ")";
return out;
}

Another one, similar to @Tony Olsson's, including a specialization for the empty tuple, as suggested by @Kerrek SB.

#include <tuple>
#include <iostream>


template<class Ch, class Tr, size_t I, typename... TS>
struct tuple_printer
{
static void print(std::basic_ostream<Ch,Tr> & out, const std::tuple<TS...> & t)
{
tuple_printer<Ch, Tr, I-1, TS...>::print(out, t);
if (I < sizeof...(TS))
out << ",";
out << std::get<I>(t);
}
};
template<class Ch, class Tr, typename... TS>
struct tuple_printer<Ch, Tr, 0, TS...>
{
static void print(std::basic_ostream<Ch,Tr> & out, const std::tuple<TS...> & t)
{
out << std::get<0>(t);
}
};
template<class Ch, class Tr, typename... TS>
struct tuple_printer<Ch, Tr, -1, TS...>
{
static void print(std::basic_ostream<Ch,Tr> & out, const std::tuple<TS...> & t)
{}
};
template<class Ch, class Tr, typename... TS>
std::ostream & operator<<(std::basic_ostream<Ch,Tr> & out, const std::tuple<TS...> & t)
{
out << "(";
tuple_printer<Ch, Tr, sizeof...(TS) - 1, TS...>::print(out, t);
return out << ")";
}

I'm surprised the implementation on cppreference has not already been posted here, so I'll do it for posterity. It's hidden in the doc for std::tuple_cat so it's not easy to find. It uses a guard struct like some of the other solutions here, but I think theirs is ultimately simpler and easier-to-follow.

#include <iostream>
#include <tuple>
#include <string>


// helper function to print a tuple of any size
template<class Tuple, std::size_t N>
struct TuplePrinter {
static void print(const Tuple& t)
{
TuplePrinter<Tuple, N-1>::print(t);
std::cout << ", " << std::get<N-1>(t);
}
};


template<class Tuple>
struct TuplePrinter<Tuple, 1> {
static void print(const Tuple& t)
{
std::cout << std::get<0>(t);
}
};


template<class... Args>
void print(const std::tuple<Args...>& t)
{
std::cout << "(";
TuplePrinter<decltype(t), sizeof...(Args)>::print(t);
std::cout << ")\n";
}
// end helper function

And a test:

int main()
{
std::tuple<int, std::string, float> t1(10, "Test", 3.14);
int n = 7;
auto t2 = std::tuple_cat(t1, std::make_pair("Foo", "bar"), t1, std::tie(n));
n = 10;
print(t2);
}

Output:

(10, Test, 3.14, Foo, bar, 10, Test, 3.14, 10)

Live Demo

Based upon example on The C++ Programming Language By Bjarne Stroustrup, page 817:

#include <tuple>
#include <iostream>
#include <string>
#include <type_traits>
template<size_t N>
struct print_tuple{
template<typename... T>static typename std::enable_if<(N<sizeof...(T))>::type
print(std::ostream& os, const std::tuple<T...>& t) {
char quote = (std::is_convertible<decltype(std::get<N>(t)), std::string>::value) ? '"' : 0;
os << ", " << quote << std::get<N>(t) << quote;
print_tuple<N+1>::print(os,t);
}
template<typename... T>static typename std::enable_if<!(N<sizeof...(T))>::type
print(std::ostream&, const std::tuple<T...>&) {
}
};
std::ostream& operator<< (std::ostream& os, const std::tuple<>&) {
return os << "()";
}
template<typename T0, typename ...T> std::ostream&
operator<<(std::ostream& os, const std::tuple<T0, T...>& t){
char quote = (std::is_convertible<T0, std::string>::value) ? '"' : 0;
os << '(' << quote << std::get<0>(t) << quote;
print_tuple<1>::print(os,t);
return os << ')';
}


int main(){
std::tuple<> a;
auto b = std::make_tuple("One meatball");
std::tuple<int,double,std::string> c(1,1.2,"Tail!");
std::cout << a << std::endl;
std::cout << b << std::endl;
std::cout << c << std::endl;
}

Output:

()
("One meatball")
(1, 1.2, "Tail!")

In C++17 we can accomplish this with a little less code by taking advantage of Fold expressions, particularly a unary left fold:

template<class TupType, size_t... I>
void print(const TupType& _tup, std::index_sequence<I...>)
{
std::cout << "(";
(..., (std::cout << (I == 0? "" : ", ") << std::get<I>(_tup)));
std::cout << ")\n";
}


template<class... T>
void print (const std::tuple<T...>& _tup)
{
print(_tup, std::make_index_sequence<sizeof...(T)>());
}

Live Demo outputs:

(5, Hello, -0.1)

given

auto a = std::make_tuple(5, "Hello", -0.1);
print(a);

Explanation

Our unary left fold is of the form

... op pack

where op in our scenario is the comma operator, and pack is the expression containing our tuple in an unexpanded context like:

(..., (std::cout << std::get<I>(myTuple))

So if I have a tuple like so:

auto myTuple = std::make_tuple(5, "Hello", -0.1);

And a std::integer_sequence whose values are specified by a non-type template (see above code)

size_t... I

Then the expression

(..., (std::cout << std::get<I>(myTuple))

Gets expanded into

((std::cout << std::get<0>(myTuple)), (std::cout << std::get<1>(myTuple))), (std::cout << std::get<2>(myTuple));

Which will print

5Hello-0.1

Which is gross, so we need to do some more trickery to add a comma separator to be printed first unless it's the first element.

To accomplish that, we modify the pack portion of the fold expression to print " ," if the current index I is not the first, hence the (I == 0? "" : ", ") portion*:

(..., (std::cout << (I == 0? "" : ", ") << std::get<I>(_tup)));

And now we'll get

5, Hello, -0.1

Which looks nicer (Note: I wanted similar output as this answer)

*Note: You could do the comma separation in a variety of ways than what I ended up with. I initially added commas conditionally after instead of before by testing against std::tuple_size<TupType>::value - 1, but that was too long, so I tested instead against sizeof...(I) - 1, but in the end I copied Xeo and we ended up with what I've got.

Based on AndyG code, for C++17

#include <iostream>
#include <tuple>


template<class TupType, size_t... I>
std::ostream& tuple_print(std::ostream& os,
const TupType& _tup, std::index_sequence<I...>)
{
os << "(";
(..., (os << (I == 0 ? "" : ", ") << std::get<I>(_tup)));
os << ")";
return os;
}


template<class... T>
std::ostream& operator<< (std::ostream& os, const std::tuple<T...>& _tup)
{
return tuple_print(os, _tup, std::make_index_sequence<sizeof...(T)>());
}


int main()
{
std::cout << "deep tuple: " << std::make_tuple("Hello",
0.1, std::make_tuple(1,2,3,"four",5.5), 'Z')
<< std::endl;
return 0;
}

with output:

deep tuple: (Hello, 0.1, (1, 2, 3, four, 5.5), Z)

Leveraging on std::apply (C++17) we can drop the std::index_sequence and define a single function:

#include <tuple>
#include <iostream>


template<class Ch, class Tr, class... Args>
auto& operator<<(std::basic_ostream<Ch, Tr>& os, std::tuple<Args...> const& t) {
std::apply([&os](auto&&... args) {((os << args << " "), ...);}, t);
return os;
}

Or, slightly embellished with the help of a stringstream:

#include <tuple>
#include <iostream>
#include <sstream>


template<class Ch, class Tr, class... Args>
auto& operator<<(std::basic_ostream<Ch, Tr>& os, std::tuple<Args...> const& t) {
std::basic_stringstream<Ch, Tr> ss;
ss << "[ ";
std::apply([&ss](auto&&... args) {((ss << args << ", "), ...);}, t);
ss.seekp(-2, ss.cur);
ss << " ]";
return os << ss.str();
}

I like DarioP's answer, but stringstream uses heap. This can be avoided:

template <class... Args>
std::ostream& operator<<(std::ostream& os, std::tuple<Args...> const& t) {
os << "(";
bool first = true;
std::apply([&os, &first](auto&&... args) {
auto print = [&] (auto&& val) {
if (!first)
os << ",";
(os << " " << val);
first = false;
};
(print(args), ...);
}, t);
os << " )";
return os;
}

One thing I dislike about the previous answers that use fold expressions is that they use index sequences or flags to keep track of the first element, which removes much of the benefit of nice clean fold expressions.

Here is an example that does not need indexing, but achieves a similar result. (Not as sophisticated as some of the others, but more could be added.)

The technique is to use what the fold already gives you: a special case for one element. I.e., one element fold just expands to elem[0], then 2 elements is elem[0] + elem[1], where + is some operation. What we want is for one element to write just that element to the stream, and for more elements, do the same, but join each one with an additional write of ", ". So mapping this on to the c++ fold, we want each element to be the action of writing some object to the stream. We want our + operation to be to intersperse two writes with a ", " write. So first transform our tuple sequence into a sequence of write actions, CommaJoiner I have called it, then for that action add an operator+ to join two actions in the way we want, adding a ", " in between:

#include <tuple>
#include <iostream>


template <typename T>
struct CommaJoiner
{
T thunk;
explicit CommaJoiner(const T& t) : thunk(t) {}


template <typename S>
auto operator+(CommaJoiner<S> const& b) const
{
auto joinedThunk = [a=this->thunk, b=b.thunk] (std::ostream& os) {
a(os);
os << ", ";
b(os);
};
return CommaJoiner<decltype(joinedThunk)>{joinedThunk};
}


void operator()(std::ostream& os) const
{
thunk(os);
}


};


template <typename ...Ts>
std::ostream& operator<<(std::ostream& os, std::tuple<Ts...> tup)
{
std::apply([&](auto ...ts) {
return (... + CommaJoiner{[=](auto&os) {os << ts;}});}, tup)(os);


return os;
}


int main() {
auto tup = std::make_tuple(1, 2.0, "Hello");
std::cout << tup << std::endl;
}

A cursory glance at godbolt suggests that this compiles quite well too, all the thunks calls being flattened.

This will need a second overload to deal with an empty tuple however.

Here's some code I recently made for printing a tuple.

#include <iostream>
#include <tuple>


using namespace std;


template<typename... Ts>
ostream& operator<<(ostream& output, const tuple<Ts...> t) {
output << '(';
apply([&](auto&&... args) {
((cout << args << ", "), ...);
}, t);
output << "\b\b";
output << ')';
return output;
}

Using your example case:

auto a = std::make_tuple(5, "Hello", -0.1);
cout << a << '\n'; // (5, Hello, -0.1)

I see answers using std::index_sequence with C++17, however, that's not the road I'd go personally. I'd rather go for recursion and constexpr if:

#include <tuple>
#include <iostream>


template<std::size_t I, class... Ts>
void doPrintTuple(const std::tuple<Ts...>& tuples) {
if constexpr (I == sizeof...(Ts)) {
std::cout << ')';
}
else {
std::cout << std::get<I>(tuples);
if constexpr (I + 1 != sizeof...(Ts)) {
std::cout << ", ";
}
doPrintTuple<I + 1>(tuples);
}
}


template<class... Ts>
void printTuple(const std::tuple<Ts...>& tuples) {
std::cout << '(';
doPrintTuple<0>(tuples);
}


int main() {
auto tup = std::make_tuple(1, "hello", 4.5);
printTuple(tup);
}

Output:

(1, hello, 4.5)