Pretty-print std :: tuple


86

Ceci fait suite à ma question précédente sur les jolis conteneurs STL , pour lesquels nous avons réussi à développer une solution très élégante et totalement générale.


Dans cette prochaine étape, je voudrais inclure la jolie impression pour std::tuple<Args...>, en utilisant des modèles variadiques (donc c'est strictement C ++ 11). Car std::pair<S,T>, je dis simplement

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

Quelle est la construction analogue pour imprimer un tuple?

J'ai essayé divers morceaux de décompression de la pile d'arguments de modèle, de passer des index et d'utiliser SFINAE pour découvrir quand je suis au dernier élément, mais sans succès. Je ne vous charge pas de mon code cassé; la description du problème est, espérons-le, assez simple. Essentiellement, j'aimerais le comportement suivant:

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

Points bonus pour avoir inclus le même niveau de généralité (char / wchar_t, séparateurs de paires) que la question précédente!


Quelqu'un a-t-il mis du code ici dans une bibliothèque? Ou même un .hpp-avec-tout-dans lequel on pourrait saisir et utiliser?
einpoklum

@einpoklum: Peut - être cxx-prettyprint ? C'est pour ça que j'avais besoin de ce code.
Kerrek SB

1
Excellente question, et +1 pour "Je ne vous chargerai pas de mon code cassé", même si je suis surpris qu'il semble avoir réussi à repousser les hordes stupides "qu'as-tu essayé".
Don Hatch

Réponses:


78

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

Exemple en direct sur Ideone.


Pour les délimiteurs, ajoutez simplement ces spécialisations partielles:

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

et modifiez le operator<<et en print_tupleconséquence:

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

Et

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: Je teste et corrige moi-même actuellement, mais j'obtiens une sortie étrange sur Ideone.
Xeo

Je pense que vous confondez également les flux et les chaînes. Vous écrivez quelque chose qui ressemble à "std :: cout << std :: cout". En d'autres termes, TuplePrintern'a pas de fichier operator<<.
Kerrek SB

1
@Thomas: Vous ne pouvez pas simplement l'utiliser class Tuplepour la operator<<surcharge - il serait choisi pour toutes les choses. Il faudrait une contrainte, ce qui implique en quelque sorte le besoin d'une sorte d'arguments variadiques.
Xeo

1
@DanielFrey: C'est un problème résolu, les garanties liste-initialisation de gauche à droite commande: swallow{(os << get<Is>(t))...};.
Xeo

6
@Xeo J'ai emprunté votre hirondelle pour référence , si cela ne vous dérange pas.
Cubbi

19

Cela fonctionne bien en C ++ 11 (gcc 4.7). Il y a, je suis sûr, quelques pièges que je n'ai pas envisagés mais je pense que le code est facile à lire et pas compliqué. La seule chose qui peut être étrange est la structure "guard" tuple_printer qui garantit que nous nous terminons lorsque le dernier élément est atteint. L'autre chose étrange peut être sizeof ... (Types) qui renvoient le nombre de types dans le type pack. Il est utilisé pour déterminer l'indice du dernier élément (taille ... (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;
}

1
Ouais, cela semble raisonnable - peut-être avec une autre spécialisation pour le tuple vide, par souci d'exhaustivité.
Kerrek SB

@KerrekSB, Il n'y a pas de moyen simple d'imprimer des tuples en c ++ ?, en python, la fonction renvoie implicitement un tuple et vous pouvez simplement les imprimer, en c ++ afin de renvoyer les multiples variables d'une fonction dont j'ai besoin pour les emballer en utilisant std::make_tuple(). mais au moment de l'imprimer main(), il jette un tas d'erreurs !, Des suggestions sur un moyen plus simple d'imprimer les tuples?
Anu

19

En C ++ 17, nous pouvons accomplir cela avec un peu moins de code en tirant parti des expressions Fold , en particulier un pli gauche unaire:

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

Sorties de démonstration en direct :

(5, bonjour, -0,1)

donné

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

Explication

Notre pli gauche unaire est de la forme

... op pack

opdans notre scénario est l'opérateur virgule, et packest l'expression contenant notre tuple dans un contexte non développé comme:

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

Donc, si j'ai un tuple comme ça:

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

Et std::integer_sequencedont les valeurs sont spécifiées par un modèle non-type (voir code ci-dessus)

size_t... I

Puis l'expression

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

S'étend en

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

Qui imprimera

5Bonjour-0.1

Ce qui est dégoûtant, nous devons donc faire plus de supercherie pour ajouter un séparateur virgule à imprimer en premier, à moins que ce ne soit le premier élément.

Pour ce faire, nous modifions la packpartie de l'expression de repli à imprimer " ,"si l'index courant In'est pas le premier, d'où la (I == 0? "" : ", ")partie * :

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

Et maintenant nous aurons

5, bonjour, -0,1

Ce qui a l'air plus joli (Remarque: je voulais une sortie similaire à celle de cette réponse )

* Remarque: vous pouvez effectuer la séparation par virgule de différentes manières que celles avec lesquelles j'ai fini. J'ai d'abord ajouté des virgules conditionnellement après au lieu d' avant en testant contre std::tuple_size<TupType>::value - 1, mais c'était trop long, donc j'ai testé à la place sizeof...(I) - 1, mais à la fin j'ai copié Xeo et nous avons fini avec ce que j'ai.


1
Vous pouvez également utiliser if constexprpour le cas de base.
Kerrek SB

@KerrekSB: Pour décider s'il faut imprimer une virgule? Pas une mauvaise idée, j'aurais aimé qu'elle vienne en ternaire.
AndyG

Une expression conditionnelle est déjà une expression constante potentielle, donc ce que vous avez est déjà bon :-)
Kerrek SB

17

Je suis surpris que l'implémentation sur cppreference n'ait pas déjà été publiée ici, alors je vais le faire pour la postérité. Il est caché dans la documentation pour std::tuple_catdonc ce n'est pas facile à trouver. Il utilise une structure de garde comme certaines des autres solutions ici, mais je pense que la leur est finalement plus simple et plus facile à suivre.

#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

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

Production:

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

Démo en direct


4

Basé sur le code AndyG, pour 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;
}

avec sortie:

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

3

Basé sur l'exemple du langage de programmation C ++ de 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;
    }

Production:

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

3

En s'appuyant sur std::apply(C ++ 17), nous pouvons supprimer le std::index_sequenceet définir une seule fonction:

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

Ou, légèrement embelli à l'aide d'un 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();
}

1

Un autre, similaire à celui de @Tony Olsson, incluant une spécialisation pour le tuple vide, comme suggéré par @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

J'aime la réponse de DarioP, mais stringstream utilise le tas. Cela peut être évité:

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

Une chose que je n'aime pas dans les réponses précédentes qui utilisent des expressions de pliage est qu'elles utilisent des séquences d'index ou des indicateurs pour garder une trace du premier élément, ce qui supprime une grande partie des avantages des belles expressions de pliage propre.

Voici un exemple qui ne nécessite pas d'indexation, mais qui aboutit à un résultat similaire. (Pas aussi sophistiqué que certains des autres, mais d'autres pourraient être ajoutés.)

La technique consiste à utiliser ce que le pli vous donne déjà: un cas particulier pour un élément. Ie, un pli d'élément se développe simplement elem[0], puis 2 éléments sont elem[0] + elem[1], où se +trouve une opération. Ce que nous voulons, c'est qu'un élément écrive uniquement cet élément dans le flux, et pour plus d'éléments, fasse de même, mais joint chacun d'eux avec une écriture supplémentaire de ",". Donc, en mappant cela sur le pli c ++, nous voulons que chaque élément soit l'action d'écrire un objet dans le flux. Nous voulons que notre +opération soit entrecoupée de deux écritures avec un "," write. Commencez par transformer notre séquence de tuple en une séquence d'actions d'écriture, CommaJoinerje l'ai appelée, puis pour cette action, ajoutez un operator+pour joindre deux actions comme nous le souhaitons, en ajoutant un "," entre les deux:

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

Un rapide coup d'œil à godbolt suggère que cela se compile assez bien aussi, tous les appels de thunks étant aplatis.

Cependant, cela nécessitera une deuxième surcharge pour traiter un tuple vide.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.