web-dev-qa-db-fra.com

Cuple 11 tagué

Les tuples C++ 11 sont gentils, mais ils ont deux inconvénients majeurs: accéder aux membres par index est

  1. illisible
  2. difficile à maintenir (si j'ajoute un élément au milieu du tuple, je suis foutu)

Ce que je veux accomplir est essentiellement ceci

tagged_Tuple <name, std::string, age, int, email, std::string> get_record (); {/*...*/}
// And then soomewhere else

std::cout << "Age: " << get_record().get <age> () << std::endl;

Quelque chose de similaire (marquage de type) est implémenté dans boost :: property_map, mais je ne comprends pas trop comment l'implémenter dans un tuple avec un nombre arbitraire d'éléments

PS S'il vous plaît faites pas suggérez de définir une énumération avec indécies d'élément Tuple.

UPDOK, voici une motivation. Dans mes projets, je dois être capable de définir un grand nombre de nuplets différents «à la volée» et tous doivent avoir certaines fonctions et opérateurs communs. Ce n'est pas possible à réaliser avec des structs

UPD2 En fait, mon exemple est probablement un peu irréaliste à mettre en œuvre. Que dis-tu de ça?

tagged_Tuple <tag<name, std::string>, tag<age, int>, tag<email, std::string>> get_record (); {/*...*/}
// And then soomewhere else

std::cout << "Age: " << get_record().get <age> () << std::endl;
28
user1773602

Je ne suis au courant d'aucune classe existante qui effectue cela, mais il est assez facile de combiner quelque chose en utilisant un std::Tuple et une liste de types d'indexation:

#include <Tuple>
#include <iostream>

template<typename... Ts> struct typelist {
  template<typename T> using prepend = typelist<T, Ts...>;
};

template<typename T, typename... Ts> struct index;
template<typename T, typename... Ts> struct index<T, T, Ts...>:
  std::integral_constant<int, 0> {};
template<typename T, typename U, typename... Ts> struct index<T, U, Ts...>:
  std::integral_constant<int, index<T, Ts...>::value + 1> {};

template<int n, typename... Ts> struct nth_impl;
template<typename T, typename... Ts> struct nth_impl<0, T, Ts...> {
  using type = T; };
template<int n, typename T, typename... Ts> struct nth_impl<n, T, Ts...> {
  using type = typename nth_impl<n - 1, Ts...>::type; };
template<int n, typename... Ts> using nth = typename nth_impl<n, Ts...>::type;

template<int n, int m, typename... Ts> struct extract_impl;
template<int n, int m, typename T, typename... Ts>
struct extract_impl<n, m, T, Ts...>: extract_impl<n, m - 1, Ts...> {};
template<int n, typename T, typename... Ts>
struct extract_impl<n, 0, T, Ts...> { using types = typename
  extract_impl<n, n - 1, Ts...>::types::template prepend<T>; };
template<int n, int m> struct extract_impl<n, m> {
  using types = typelist<>; };
template<int n, int m, typename... Ts> using extract = typename
  extract_impl<n, m, Ts...>::types;

template<typename S, typename T> struct tt_impl;
template<typename... Ss, typename... Ts>
struct tt_impl<typelist<Ss...>, typelist<Ts...>>:
  public std::Tuple<Ts...> {
  template<typename... Args> tt_impl(Args &&...args):
    std::Tuple<Ts...>(std::forward<Args>(args)...) {}
  template<typename S> nth<index<S, Ss...>::value, Ts...> get() {
    return std::get<index<S, Ss...>::value>(*this); }
};
template<typename... Ts> struct tagged_Tuple:
  tt_impl<extract<2, 0, Ts...>, extract<2, 1, Ts...>> {
  template<typename... Args> tagged_Tuple(Args &&...args):
    tt_impl<extract<2, 0, Ts...>, extract<2, 1, Ts...>>(
      std::forward<Args>(args)...) {}
};

struct name {};
struct age {};
struct email {};

tagged_Tuple<name, std::string, age, int, email, std::string> get_record() {
  return { "Bob", 32, "[email protected]"};
}

int main() {
  std::cout << "Age: " << get_record().get<age>() << std::endl;
}

Vous aurez probablement envie d'écrire les accesseurs const et rvalue get par-dessus ceux qui existent déjà.

42
ecatmur

C++ n'a pas de type struct qui puisse être itératif comme un Tuple; c'est soit/ou.

Le plus proche que vous pouvez obtenir est par le biais de struct adapter de Boost.Fusion. Cela vous permet d'utiliser une structure en tant que séquence de fusion. Bien sûr, ceci utilise également une série de macros et vous oblige à lister explicitement les membres de la structure dans l'ordre dans lequel vous souhaitez les parcourir. Dans l'en-tête (en supposant que vous souhaitiez parcourir la structure dans de nombreuses unités de traduction).

En fait, mon exemple est probablement un peu irréaliste à mettre en œuvre. Que dis-tu de ça?

Vous pouvez implémenter quelque chose comme ça, mais ces identifiants doivent être des types, des variables ou autre chose.

8
Nicol Bolas

J'ai ma propre implémentation à montrer, ce qui peut vous permettre de ne pas déclarer les attributs en haut du fichier. Une version avec des attributs déclarés existe aussi, mais il n'est pas nécessaire de les définir, la déclaration suffit.

Bien sûr, il s’agit d’une technologie STL pure qui n’utilise pas le préprocesseur.

Exemple:

#include <named_tuples/Tuple.hpp>
#include <string>
#include <iostream>
#include <vector>

namespace {
unsigned constexpr operator "" _h(const char* c,size_t) { return named_tuples::const_hash(c); }
template <unsigned Id> using at = named_tuples::attribute_init_int_placeholder<Id>;
using named_tuples::make_Tuple;
}

int main() {
  auto test = make_Tuple( 
      at<"nom"_h>() = std::string("Roger")
      , at<"age"_h>() = 47
      , at<"taille"_h>() = 1.92
      , at<"liste"_h>() = std::vector<int>({1,2,3})
      );

  std::cout 
    << test.at<"nom"_h>() << "\n"
    << test.at<"age"_h>() << "\n"
    << test.at<"taille"_h>() << "\n"
    << test.at<"liste"_h>().size() << std::endl;

  test.at<"nom"_h>() = "Marcel";
  ++test.get<1>();

  std::cout 
    << test.get<0>() << "\n"
    << test.get<1>() << "\n"
    << test.get<2>() << "\n"
    << test.get<3>().size() << std::endl;

  return 0;
}

Trouvez la source complète ici https://github.com/duckie/named_Tuple . N'hésitez pas à lire, c'est assez simple.

7

Les vrais problèmes que vous devez résoudre ici sont:

  • Les balises sont-elles obligatoires ou facultatives?
  • Les balises sont-elles uniques? Est-il appliqué au moment de la compilation?
  • Dans quel domaine se situe la balise? Votre exemple semble déclarer les balises à l'intérieur de la portée de déclaration au lieu d'être encapsulées dans le type, ce qui peut ne pas être optimal.

ecatmur a proposé une bonne solution; mais les balises ne sont pas encapsulées et la déclaration des balises est maladroite. C++ 14 introduira Adressage des tuples par type , ce qui simplifiera sa conception et garantira l’unicité des balises, mais ne résoudra pas leur portée.

Boost Fusion Map peut également être utilisé pour quelque chose de similaire, mais encore une fois, déclarer les balises n’est pas idéal.

Il existe une proposition pour quelque chose de similaire sur le forum c ++ Standard Proposition , qui simplifierait la syntaxe en associant directement un nom au paramètre template.

Ce lien répertorie différentes manières de mettre en œuvre ceci (y compris la solution de ecatmur ) et présente un cas d'utilisation différent pour cette syntaxe.

1
Thibaut

J'ai "résolu" un problème similaire dans le code de production. Premièrement, j'ai une structure ordinaire (en fait une classe avec différentes fonctions membres, mais ce ne sont que les données membres qui nous intéressent ici) ...

class Record
{
    std::string name;
    int age;
    std::string email;
    MYLIB_ENABLE_Tuple(Record) // macro
};

Puis juste en dessous de la définition de la structure, mais en dehors de tout espace de noms, j'ai une autre macro:

MYLIB_DECLARE_Tuple(Record, (o.name, o.age, o.email))

L'inconvénient de cette approche est que les noms de membres doivent être répertoriés deux fois, mais c'est le meilleur que j'ai pu trouver tout en permettant la syntaxe d'accès de membre traditionnelle dans les fonctions membres de la structure. La macro apparaît très près des définitions des membres de données eux-mêmes. Il n'est donc pas difficile de les synchroniser les uns avec les autres.

Dans un autre fichier d'en-tête, j'ai un modèle de classe:

template <class T>
class TupleConverter;

La première macro est définie de manière à déclarer ce modèle comme étant une friend de la structure, de sorte qu'il puisse accéder à ses membres de données privés:

#define MYLIB_ENABLE_Tuple(TYPE) friend class TupleConverter<TYPE>;

La deuxième macro est définie de manière à introduire une spécialisation du modèle:

#define MYLIB_DECLARE_Tuple(TYPE, MEMBERS) \
    template <>                            \
    class TupleConverter<TYPE>             \
    {                                      \
        friend class TYPE;                 \
        static auto toTuple(TYPE& o)       \
            -> decltype(std::tie MEMBERS)  \
        {                                  \
            return std::tie MEMBERS;       \
        }                                  \
    public:                                \
        static auto toTuple(TYPE const& o) \
            -> decltype(std::tie MEMBERS)  \
        {                                  \
            return std::tie MEMBERS;       \
        }                                  \
    };

Cela crée deux surcharges du même nom de fonction membre, TupleConverter<Record>::toTuple(Record const&) qui est public et TupleConverter<Record>::toTuple(Record&), qui est privé et accessible uniquement à Record lui-même par amitié. Les deux renvoient leur argument converti en un tuple de références à des membres de données privées au moyen de std::tie. La surcharge publique const renvoie un tuple de références à const, la surcharge privée non const renvoie un tuple de références à non const.

Après la substitution du préprocesseur, les deux déclarations friend font référence à des entités définies dans le même fichier d’en-tête. Il ne devrait donc pas y avoir de risque que d’autres codes exploitent l’amitié pour rompre l’encapsulation.

toTuple ne peut pas être une fonction membre de Record, car son type de retour ne peut pas être déduit tant que la définition de Record n'est pas terminée.

L'utilisation typique ressemble à ceci:

// lexicographical comparison
bool operator< (Record const& a, Record const& b)
{
    return TupleConverter<Record>::toTuple(a) < TupleConverter<Record>::toTuple(b);
}

// serialization
std::ostream& operator<< (std::ostream& os, Record const& r)
{
    // requires template<class... Ts> ostream& operator<<(ostream&, Tuple<Ts...>) defined elsewhere
    return os << TupleConverter<Record>::toTuple(r);
}

Cela peut être étendu de plusieurs manières, par exemple en ajoutant une autre fonction membre dans TupleConverter qui renvoie un std::vector<std::string> du nom des membres de données.

Si j'avais été autorisé à utiliser des macros variadiques, la solution aurait peut-être été encore meilleure.

1
Oktalist

J'ai implémenté "c ++ nommé Tuple" en utilisant le préprocesseur boost. Veuillez consulter l'exemple d'utilisation ci-dessous. En dérivant de Tuple, je reçois gratuitement la comparaison, l’impression, le hachage et la sérialisation (en supposant qu’ils soient définis pour Tuple).

#include <boost/preprocessor/seq/for_each_i.hpp>
#include <boost/preprocessor/comma_if.hpp>


#define CM_NAMED_Tuple_ELEMS_ITR(r, xxx, index, x ) BOOST_PP_COMMA_IF(index) BOOST_PP_Tuple_ELEM(2,0,x) 
#define CM_NAMED_Tuple_ELEMS(seq) BOOST_PP_SEQ_FOR_EACH_I(CM_NAMED_Tuple_ELEMS_ITR, "dum", seq)
#define CM_NAMED_Tuple_PROPS_ITR(r, xxx, index, x) \
      BOOST_PP_Tuple_ELEM(2,0,x) BOOST_PP_CAT(get_, BOOST_PP_Tuple_ELEM(2,1,x))() const { return get<index>(*this); } \
      void BOOST_PP_CAT(set_, BOOST_PP_Tuple_ELEM(2,1,x))(const BOOST_PP_Tuple_ELEM(2,0,x)& oo) { get<index>(*this) = oo; }
#define CM_NAMED_Tuple_PROPS(seq) BOOST_PP_SEQ_FOR_EACH_I(CM_NAMED_Tuple_PROPS_ITR, "dum", seq)
#define cm_named_Tuple(Cls, seq) struct Cls : Tuple< CM_NAMED_Tuple_ELEMS(seq)> { \
        typedef Tuple<CM_NAMED_Tuple_ELEMS(seq)> Base;                      \
        Cls() {}                                                            \
        template<class...Args> Cls(Args && ... args) : Base(args...) {}     \
        struct hash : std::hash<CM_NAMED_Tuple_ELEMS(seq)> {};            \
        CM_NAMED_Tuple_PROPS(seq)                                           \
        template<class Archive> void serialize(Archive & ar, arg const unsigned int version)() {                                                    \
          ar & boost::serialization::base_object<Base>(*this);                              \
        }                                                                   \
      }

//
// Example:
//
// class Sample {
//   public:
//   void do_tata() {
//     for (auto& dd : bar2_) {
//       cout << dd.get_from() << " " << dd.get_to() << dd.get_tata() << "\n";
//       dd.set_tata(dd.get_tata() * 5);
//     }
//     cout << bar1_ << bar2_ << "\n";
//   }
//
//   cm_named_Tuple(Foo, ((int, from))((int, to))((double, tata)));  // Foo == Tuple<int,int,double> with named get/set functions
//
//   unordered_set<Foo, Foo::hash> bar1_;
//   vector<Foo> bar2_;  
// };

Veuillez noter que l'exemple de code ci-dessus suppose que vous avez défini les fonctions d'impression "génériques" d'ostream pour vector/Tuple/unordered_set.

1
Dr.Altan

Voici une autre façon de le faire, c'est un peu plus laid de définir les types, mais cela permet d'éviter les erreurs lors de la compilation car vous définissez les paires avec une classe type_pair (un peu comme std::map). La prochaine étape consiste à ajouter une vérification pour vous assurer que vos clés/noms sont uniques au moment de la compilation.

Usage:

   using user_t = tagged_Tuple<type_pair<struct name, std::string>, type_pair<struct age, int>>;
  // it's initialized the same way as a Tuple created with the value types of the type pairs (so Tuple<string, int> in this case)
  user_t user  { "chris", 21 };
  std::cout << "Name: " << get<name>(user) << std::endl;
  std::cout << "Age: " << get<age>(user) << std::endl;
 // you can still access properties via numeric indexes as if the class was defined as Tuple<string, int>
  std::cout << "user[0] = " << get<0>(user) << std::endl;

J'ai opté contre le fait que get soit une fonction membre pour que ce soit aussi similaire que possible à std :: Tuple, mais vous pouvez facilement en ajouter une à la classe. Code source ici

1
Christopher Tarquini

Voici une implémentation similaire à la réponse ecatmur utilisant la bibliothèque de métaprogrammation brigand ( https://github.com/edouarda/brigand ):

#include <iostream>
#include <brigand/brigand.hpp>

template<typename Members>
class TaggedTuple{

    template<typename Type>
    struct createMember{
        using type = typename Type::second_type;
    };

    using DataTuple = brigand::transform<Members, createMember<brigand::_1>>;
    using Keys = brigand::keys_as_sequence<Members, brigand::list>;
    brigand::as_Tuple<DataTuple> members;

public:

    template<typename TagType>
    auto& get(){
        using index = brigand::index_of<Keys, TagType>;
        return std::get<index::value>(members);
    }
};

int main(){

    struct FloatTag{};
    struct IntTag{};
    struct DoubleTag{};

    TaggedTuple<brigand::map<
            brigand::pair<FloatTag, float>,
            brigand::pair<IntTag, int>,
            brigand::pair<DoubleTag, double>>> tagged;

    tagged.get<DoubleTag>() = 200;
    auto val = tagged.get<DoubleTag>();
    std::cout << val << std::endl;

    return 0;
}
0
underdoeg