Bản in đẹp std :: tuple


86

Đây là phần tiếp theo cho câu hỏi trước đây của tôi về các thùng chứa STL in đẹp , mà chúng tôi đã cố gắng phát triển một giải pháp chung rất thanh lịch và đầy đủ.


Trong bước tiếp theo này, tôi muốn bao gồm việc in đẹp std::tuple<Args...>, sử dụng các mẫu đa dạng (vì vậy đây hoàn toàn là C ++ 11). Đối với std::pair<S,T>, tôi chỉ đơn giản nói

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

Cấu trúc tương tự để in một bộ tuple là gì?

Tôi đã thử giải nén nhiều bit khác nhau của ngăn xếp đối số mẫu, chuyển các chỉ số xung quanh và sử dụng SFINAE để khám phá khi nào tôi đang ở phần tử cuối cùng, nhưng không thành công. Tôi sẽ không tạo gánh nặng cho bạn với mã bị hỏng của tôi; mô tả vấn đề hy vọng là đủ thẳng. Về cơ bản, tôi muốn hành vi sau:

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

Điểm thưởng khi bao gồm cùng mức độ tổng quát (char / wchar_t, dấu phân cách cặp) như câu hỏi trước!


Ai đó đã đặt bất kỳ mã nào ở đây vào thư viện? Hay thậm chí là .hpp-with-everything-in mà người ta có thể lấy và sử dụng?
einpoklum,

@einpoklum: Có lẽ là cxx-prettyprint ? Đó là những gì tôi cần mã đó.
Kerrek SB,

1
Câu hỏi tuyệt vời và +1 cho "Tôi không làm bạn gánh nặng vì mã bị hỏng của tôi", mặc dù tôi rất ngạc nhiên là có vẻ như nó đã thực sự thành công trong việc chống lại đám "bạn đã thử" vô tâm.
Don Hatch

Câu trả lời:


78

Yay, các chỉ số ~

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

Ví dụ trực tiếp trên Ideone.


Đối với nội dung dấu phân cách, chỉ cần thêm các chuyên môn từng phần sau:

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

và thay đổi operator<<và cho print_tuplephù hợp:

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

@Kerrek: Tôi hiện đang tự kiểm tra và sửa lỗi, tôi nhận được kết quả kỳ lạ trên Ideone.
Xeo

Tôi nghĩ rằng bạn cũng đang nhầm lẫn giữa các luồng và chuỗi. Bạn đang viết một cái gì đó tương tự như "std :: cout << std :: cout". Nói cách khác, TuplePrinterkhông có operator<<.
Kerrek SB

1
@Thomas: Bạn không thể chỉ sử dụng class Tuplecho operator<<quá tải - nó sẽ được chọn cho bất kỳ và tất cả mọi thứ. Nó sẽ cần một ràng buộc, nghĩa là cần phải có một số loại đối số khác nhau.
Xeo

1
@DanielFrey: Đó là một vấn đề đã được giải quyết, việc khởi tạo danh sách đảm bảo thứ tự từ trái sang phải : swallow{(os << get<Is>(t))...};.
Xeo

6
@Xeo Tôi đã mượn con chim én của bạn để lấy ý kiến , nếu bạn không phiền.
Cubbi

19

Tôi nhận thấy điều này hoạt động tốt trong C ++ 11 (gcc 4.7). Tôi chắc chắn rằng một số cạm bẫy tôi đã không xem xét nhưng tôi nghĩ rằng mã rất dễ đọc và không phức tạp. Điều duy nhất có thể lạ là struct tuple_printer "bảo vệ" đảm bảo rằng chúng ta kết thúc khi đạt đến phần tử cuối cùng. Điều kỳ lạ khác có thể là sizeof ... (Các loại) trả về số loại trong gói loại Loại. Nó được sử dụng để xác định chỉ số của phần tử cuối cùng (kích thước ... (Các loại) - 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;
}

1
Vâng, điều đó có vẻ hợp lý - có lẽ với một chuyên môn khác cho bộ trống rỗng, để hoàn thiện.
Kerrek SB

@KerrekSB, Không có cách đơn giản nào để in các bộ giá trị trong c ++ ?, trong hàm python hoàn toàn trả về một bộ giá trị và bạn có thể chỉ cần in chúng, trong c ++ để trả về nhiều biến từ một hàm mà tôi cần đóng gói chúng bằng cách sử dụng std::make_tuple(). nhưng tại thời điểm in nó vào main(), nó phát ra một loạt lỗi !, Bất kỳ đề xuất nào về cách đơn giản hơn để in các bộ giá trị?
Anu

19

Trong C ++ 17, chúng ta có thể thực hiện điều này với ít mã hơn một chút bằng cách tận dụng các biểu thức Fold , đặc biệt là nếp gấp trái một lần:

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

Đầu ra Demo trực tiếp :

(5, Xin chào, -0.1)

được

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

Giải trình

Nếp gấp bên trái một lần của chúng tôi có dạng

... op pack

optrong đó trong kịch bản của chúng ta là toán tử dấu phẩy và packlà biểu thức chứa bộ giá trị của chúng ta trong ngữ cảnh không được mở rộng như:

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

Vì vậy, nếu tôi có một tuple như vậy:

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

Và một std::integer_sequencecó giá trị được chỉ định bởi một mẫu không phải loại (xem mã ở trên)

size_t... I

Sau đó, biểu thức

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

Được mở rộng thành

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

Cái nào sẽ in

5Xin chào-0,1

Đó là tổng, vì vậy chúng tôi cần thực hiện thêm một số thủ thuật để thêm dấu phân tách dấu phẩy sẽ được in trước trừ khi đó là phần tử đầu tiên.

Để thực hiện điều đó, chúng tôi sửa đổi packphần của biểu thức màn hình đầu tiên để in " ,"nếu chỉ mục hiện tại Ikhông phải là chỉ mục đầu tiên, do đó là (I == 0? "" : ", ")phần * :

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

Và bây giờ chúng ta sẽ nhận được

5, Xin chào, -0.1

Cái nào trông đẹp hơn (Lưu ý: Tôi muốn đầu ra tương tự như câu trả lời này )

* Lưu ý: Bạn có thể thực hiện phân tách dấu phẩy theo nhiều cách khác nhau so với những gì tôi đã kết thúc. Ban đầu tôi đã thêm dấu phẩy có điều kiện vào sau thay vì trước đó bằng cách thử nghiệm chống lại std::tuple_size<TupType>::value - 1, nhưng điều đó quá dài, vì vậy tôi đã thử nghiệm thay thế chống lại sizeof...(I) - 1, nhưng cuối cùng tôi đã sao chép Xeo và chúng tôi đã kết thúc với những gì tôi có.


1
Bạn cũng có thể sử dụng if constexprcho trường hợp cơ sở.
Kerrek SB

@KerrekSB: Để quyết định có nên in dấu phẩy không? Không phải là một ý tưởng tồi, ước gì nó đến trong thế hệ thứ ba.
AndyG

Một biểu thức điều kiện đã là một biểu thức hằng số tiềm năng, vì vậy những gì bạn có là đã tốt :-)
Kerrek SB

17

Tôi ngạc nhiên là việc triển khai trên cppreference vẫn chưa được đăng ở đây, vì vậy tôi sẽ thực hiện nó cho hậu thế. Nó được ẩn trong tài liệu std::tuple_catnên không dễ tìm. Nó sử dụng cấu trúc bảo vệ giống như một số giải pháp khác ở đây, nhưng tôi nghĩ rằng chúng cuối cùng đơn giản hơn và dễ thực hiện hơn.

#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

Và một bài kiểm tra:

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

Đầu ra:

(10, Thử nghiệm, 3,14, Foo, thanh, 10, Thử nghiệm, 3,14, 10)

Bản thử trực tiếp


4

Dựa trên mã AndyG, cho 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;
}

với đầu ra:

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

3

Dựa trên ví dụ về Ngôn ngữ lập trình C ++ của Bjarne Stroustrup, trang 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;
    }

Đầu ra:

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

3

Tận dụng std::apply(C ++ 17), chúng ta có thể loại bỏ std::index_sequencevà xác định một hàm duy nhất:

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

Hoặc, được tô điểm một chút với sự trợ giúp của chuỗi chuỗi:

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

1

Một cái khác, tương tự như @Tony Olsson, bao gồm một chuyên môn cho bộ trống, theo đề xuất của @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 << ")";
}

0

Tôi thích câu trả lời của DarioP, nhưng stringstream sử dụng heap. Điều này có thể tránh được:

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

0

Một điều tôi không thích ở các câu trả lời trước sử dụng biểu thức gấp là chúng sử dụng chuỗi chỉ mục hoặc cờ để theo dõi phần tử đầu tiên, điều này làm mất đi nhiều lợi ích của các biểu thức nếp gấp đẹp mắt.

Đây là một ví dụ không cần lập chỉ mục, nhưng đạt được một kết quả tương tự. (Không phức tạp như một số người khác, nhưng có thể thêm nhiều thứ khác).

Kỹ thuật này là sử dụng những gì mà nếp gấp đã cung cấp cho bạn: một trường hợp đặc biệt cho một phần tử. Tức là, một nếp gấp phần tử chỉ mở rộng thành elem[0], sau đó 2 phần tử là elem[0] + elem[1], +một số hoạt động ở đâu. Những gì chúng tôi muốn là đối với một phần tử chỉ ghi phần tử đó vào luồng và đối với các phần tử khác, hãy làm như vậy, nhưng nối mỗi phần tử với một chữ viết bổ sung là ",". Vì vậy, ánh xạ điều này vào nếp gấp c ++, chúng tôi muốn mỗi phần tử là hành động ghi một số đối tượng vào luồng. Chúng tôi muốn +hoạt động của chúng tôi là xen kẽ hai lần viết với một "," viết. Vì vậy, trước tiên hãy chuyển đổi chuỗi tuple của chúng ta thành một chuỗi các hành động ghi, CommaJoinertôi đã gọi nó, sau đó đối với hành động đó, hãy thêm một operator+để nối hai hành động theo cách chúng ta muốn, thêm dấu "," vào giữa:

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

Một cái nhìn lướt qua về chốt thần cho thấy rằng điều này cũng được biên dịch khá tốt, tất cả các cuộc gọi côn đồ đang được làm phẳng.

Tuy nhiên, điều này sẽ cần quá tải thứ hai để đối phó với một tuple trống.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.