93

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


在下一步中,我想std::tuple<Args...>使用可变参数模板包括漂亮的打印(所以这是严格的 C++11)。对于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,对分隔符)的奖励积分!

4

12 回答 12

79

耶,指数~

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

Ideone 上的实时示例。


对于分隔符的东西,只需添加这些部分特化:

// 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")" };

并相应地更改operator<<and print_tuple

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

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)...};
}
于 2011-06-05T20:52:10.433 回答
26

在 C++17 中,我们可以通过利用折叠表达式,特别是一元左折叠,用更少的代码完成此操作:

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

现场演示输出:

(5,你好,-0.1)

给定

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

解释

我们的一元左折叠的形式是

... op pack

op我们的场景中是逗号运算符,并且pack是在未扩展的上下文中包含我们的元组的表达式,例如:

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

所以如果我有一个像这样的元组:

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

和 a std::integer_sequence,其值由非类型模板指定(参见上面的代码)

size_t... I

然后表达式

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

扩展为

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

哪个会打印

5你好-0.1

这很恶心,所以我们需要做更多的技巧来添加一个逗号分隔符来首先打印,除非它是第一个元素。

为了实现这一点,如果当前索引不是第一个,我们修改pack折叠表达式的部分以打印,因此部分*" ,"I(I == 0? "" : ", ")

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

现在我们会得到

5、你好,-0.1

哪个看起来更好(注意:我想要与此答案类似的输出)

*注意:您可以通过多种方式进行逗号分隔,而不是我最终的方式。我最初通过测试而不是之前有条件添加了逗号,但那太长了,所以我测试了而不是测试,但最后我复制了Xeo,我们最终得到了我所拥有的。std::tuple_size<TupType>::value - 1sizeof...(I) - 1

于 2016-12-15T19:05:17.460 回答
19

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;
}
于 2013-07-04T14:37:33.543 回答
18

我很惊讶cppreference的实现还没有在这里发布,所以我会为后代做。它隐藏在文档中,std::tuple_cat因此不容易找到。它像这里的其他一些解决方案一样使用保护结构,但我认为他们的最终更简单,更易于遵循。

#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

和一个测试:

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

输出:

(10, 测试, 3.14, Foo, bar, 10, 测试, 3.14, 10)

现场演示

于 2015-06-29T12:59:19.030 回答
5

利用std::apply(C++17) 我们可以删除std::index_sequence并定义一个函数:

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

或者,在字符串流的帮助下稍微修饰一下:

#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();
}
于 2019-10-16T15:52:30.740 回答
4

基于 AndyG 代码,用于 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;
}

输出:

deep tuple: (Hello, 0.1, (1, 2, 3, four, 5.5), Z)
于 2019-01-26T22:06:03.543 回答
3

基于Bjarne Stroustrup 撰写的 C++ 编程语言中的示例,第 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;
    }

输出:

()
("One meatball")
(1, 1.2, "Tail!")
于 2015-11-12T23:22:30.180 回答
1

另一个类似于@Tony Olsson 的,包括空元组的专门化,正如@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 << ")";
}
于 2014-12-30T11:13:27.863 回答
1

我看到使用 C++17 的答案std::index_sequence,但是,这不是我个人要走的路。我宁愿去递归和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);
}

输出:

(1, hello, 4.5)
于 2021-05-25T11:50:30.860 回答
1

我喜欢 DarioP 的回答,但 stringstream 使用堆。这是可以避免的:

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;
}
于 2020-05-13T21:10:21.473 回答
0

这是我最近为打印元组而编写的一些代码。

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

使用您的示例案例:

auto a = std::make_tuple(5, "Hello", -0.1); 
cout << a << '\n'; // (5, Hello, -0.1)
于 2020-11-30T00:37:41.900 回答
0

关于使用折叠表达式的先前答案,我不喜欢的一点是它们使用索引序列或标志来跟踪第一个元素,这消除了漂亮干净折叠表达式的大部分好处。

这是一个不需要索引的示例,但可以实现类似的结果。(不像其他一些那样复杂,但可以添加更多。)

该技术是使用折叠已经给你的东西:一个元素的特殊情况。即,一个元素折叠只是展开到elem[0],然后 2 个元素是elem[0] + elem[1],其中+是一些操作。我们想要的是让一个元素只将那个元素写入流中,而对于更多元素,也这样做,但将每个元素与额外的“,”写入连接起来。因此,将其映射到 c++ 折叠,我们希望每个元素都是将某个对象写入流的动作。我们希望我们的+操作是在两个写入之间穿插一个“,”写入。因此,首先将我们的元组序列转换为一系列写入操作,CommaJoiner我称之为它,然后为该操作添加一个operator+以我们想要的方式连接两个操作,在其间添加一个“,”:

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

粗略地看一下 godbolt 表明它编译得也很好,所有的 thunk 调用都被展平了。

然而,这将需要第二次重载来处理空元组。

于 2020-06-11T23:43:20.187 回答