简体   繁体   English

C++11 标记元组

[英]C++11 Tagged Tuple

C++11 tuples are nice, but they have two huge disadvantages to me, accessing members by index is C++11 元组很好,但它们对我来说有两个巨大的缺点,通过索引访问成员是

  1. unreadable不可读
  2. difficult to maintain (if I add an element in the middle of the tuple, I'm screwed)难以维护(如果我在元组中间添加一个元素,我就搞砸了)

In essence what I want to achieve is this本质上我想要实现的是这个

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;

Something similar (type tagging) is implemented in boost::property_map, but I ca'nt get my head around how to implement it in a tuple with arbitary number of elements在 boost::property_map 中实现了类似的东西(类型标记),但我无法理解如何在具有任意数量元素的元组中实现它

PS Please do not suggest defining an enum with tuple element indices. PS请不要建议定义与元组元素索引枚举。

UPD OK, here is a motivation. UPD好的,这是一个动机。 In my projects I need to be able to define lots of different tuples 'on-the-fly' and all of them need to have certain common functions and operators.在我的项目中,我需要能够“即时”定义许多不同的元组,并且所有这些元组都需要具有某些通用功能和运算符。 This is not possible to achieve with structs这是不可能用结构实现的

UPD2 Actually my example is probably a bit unrealistic to implement. UPD2其实我的例子实现起来可能有点不切实际。 How about this?这个怎么样?

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

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

I'm not aware of any existing class that does this, but it's fairly easy to throw something together using a std::tuple and an indexing typelist:我不知道有任何现有的类这样做,但是使用std::tuple和索引类型列表将一些东西放在一起是相当容易的:

#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, "bob@bob.bob"};
}

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

You'll probably want to write const and rvalue get accessors on top of the existing one.您可能希望在现有访问器之上编写const和 rvalue get访问器。

C++ does not have a struct type that can be iteratable like a tuple ; C++ 没有像tuple那样可以迭代的struct类型; it's either/or.它是/或。

The closest you can get to that is through Boost.Fusion's struct adapter .最接近的是通过 Boost.Fusion 的struct adapter This allows you to use a struct as a Fusion sequence.这允许您将结构用作 Fusion 序列。 Of course, this also uses a series of macros, and it requires you to list the struct's members explicitly in the order you want to iterate over them.当然,这也使用了一系列宏,它要求您按照要遍历它们的顺序显式列出结构的成员。 In the header (assuming you want to iterate over the struct in many translation units).在标题中(假设您想在许多翻译单元中迭代结构)。

Actually my example is probably a bit unrealistic to implement.实际上,我的示例实施起来可能有点不切实际。 How about this?这个怎么样?

You could implement something like that, but those identifiers need to actually be types or variables or something.你可以实现类似的东西,但这些标识符实际上需要是类型或变量或其他东西。

I have my own implementation to show off, wich can allow you not to declare the attributes on top of the file.我有我自己的实现来炫耀,它可以让你不要在文件顶部声明属性。 A version with declared attributes exists too, but there is no need to define them, declaration is sufficient.也有一个带有声明属性的版本,但是不需要定义它们,声明就足够了。

It is pure STL, of course, and do not use the preprocessor.当然,它是纯 STL,不使用预处理器。

Example:例子:

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

Find the complete source here https://github.com/duckie/named_tuple .在这里找到完整的源https://github.com/duckie/named_tuple Feel free to read, it is quite simple.随意阅读,这很简单。

The real problems you have to solve here are:您必须在这里解决的实际问题是:

  • Are the tags mandatory or optional?标签是强制性的还是可选的?
  • Are the tags unique?标签是唯一的吗? Is it enforced at compile time?它在编译时强制执行吗?
  • In which scope does the tag reside?标签位于哪个范围内? Your example seems to declare the tags inside the declaring scope instead of encapsulated in the type, which might not be optimal.您的示例似乎在声明范围内声明了标签,而不是封装在类型中,这可能不是最佳选择。

ecatmur proposed a good solution; ecatmur提出了一个很好的解决方案; but the tags are not encapsulated and the tag declaration is somehow clumsy.但是标签没有封装,标签声明有点笨拙。 C++14 will introduce tuple addressing by type , which will simplify his design and guarantee uniqueness of the tags, but not solve their scope. C++14 将引入按类型寻址的元组,这将简化他的设计并保证标签的唯一性,但不能解决它们的范围。

Boost Fusion Map can also be used for something similar, but again, declaring the tags is not ideal. Boost Fusion Map也可以用于类似的事情,但同样,声明标签并不理想。

There is a proposal for something similar on the c++ Standard Proposal forum , which would simplify the syntax by associating a name to the template parameter directly.c++ Standard Proposal forum上有一个类似的提案,它可以通过将名称直接关联到模板参数来简化语法。

This link lists different ways of implementing this (including ecatmur 's solution) and presents a different use-case for this syntax.此链接列出了实现此目的的不同方法(包括ecatmur的解决方案),并为此语法提供了不同的用例。

Here's another way to do it, it's a bit uglier to define the types but it helps prevent errors at compile time because you define the pairs with a type_pair class (much like std::map ).这是另一种方法,定义类型有点难看,但它有助于在编译时防止错误,因为您使用type_pair类(很像std::map )定义对。 Adding a check to make sure your keys/name are unique at compile time is the next step下一步是添加检查以确保您的键/名称在编译时是唯一的

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;

I opted against having get be a member function to keep it as similar to std::tuple as possible but you could easily add one to the class.我选择不让 get 成为成员函数以使其尽可能类似于 std::tuple ,但您可以轻松地向类中添加一个。 Source code here源代码在这里

Here is an implementation similar to ecatmur's answer using the brigand metaprogramming library ( https://github.com/edouarda/brigand ):这是使用 brigand 元编程库 ( https://github.com/edouarda/brigand ) 的类似于 ecatmur 的答案的实现:

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

I implemented "c++ named tuple" using boost preprocessor.我使用boost预处理器实现了“c++命名元组”。 Please see the Sample usage below.请参阅下面的示例用法。 By deriving from tuple, I get comparison, printing, hash, serialization for free (assuming they are defined for 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_;  
// };

Please note that code sample above assumes you have defined "generic" ostream printing functions for vector/tuple/unordered_set.请注意,上面的代码示例假定您已经为 vector/tuple/unordered_set 定义了“通用”ostream 打印函数。

I have "solved" a similar problem in production code.我在生产代码中“解决”了一个类似的问题。 First, I have an ordinary struct (actually a class with various member functions, but it's only the data members which we are interested in here)...首先,我有一个普通的结构体(实际上是一个具有各种成员函数的类,但它只是我们在这里感兴趣的数据成员)...

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

Then just below the struct definition, but outside of any namespace, I have another macro:然后就在结构定义的下方,但在任何命名空间之外,我还有另一个宏:

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

The disadvantage with this approach is that the member names must be listed twice, but this is the best I have been able to come up with while still permitting traditional member access syntax within the struct's own member functions.这种方法的缺点是成员名称必须列出两次,但这是我能想到的最好的方法,同时仍然允许在结构自己的成员函数中使用传统的成员访问语法。 The macro appears very near the definitions of the data members themselves, so it is not too hard to keep them in sync with each other.宏出现在数据成员本身的定义附近,因此保持它们彼此同步并不难。

In another header file I have a class template:在另一个头文件中,我有一个类模板:

template <class T>
class TupleConverter;

The first macro is defined so as to declare this template to be a friend of the struct, so it can access its private data members:定义第一个宏是为了将这个模板声明为结构体的friend ,因此它可以访问其私有数据成员:

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

The second macro is defined so as to introduce a specialization of the template:定义第二个宏以引入模板的特殊化:

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

This creates two overloads of the same member function name, TupleConverter<Record>::toTuple(Record const&) which is public, and TupleConverter<Record>::toTuple(Record&) which is private and accessible only to Record itself through friendship.这会创建两个具有相同成员函数名称的重载, TupleConverter<Record>::toTuple(Record const&)是公共的,而TupleConverter<Record>::toTuple(Record&)是私有的并且只能通过友谊来访问Record本身。 Both return their argument converted to a tuple of references to private data members by way of std::tie .两者都通过std::tie返回转换为对私有数据成员的引用的元组的参数。 The public const overload returns a tuple of references to const, the private non-const overload returns a tuple of references to non-const.公共 const 重载返回对 const 的引用的元组,私有的非常量重载返回对非常量的引用的元组。

After preprocessor substitution, both friend declarations refer to entities defined in the same header file, so there should be no chance of other code abusing the friendship to break encapsulation.在预处理器替换之后,两个friend声明都引用了在同一个头文件中定义的实体,因此应该没有其他代码滥用友元来破坏封装的机会。

toTuple can't be a member function of Record , because its return type can't be deduced until the definition of Record is complete. toTuple不能是Record的成员函数,因为在Record的定义完成之前无法推导出它的返回类型。

Typical usage looks like this:典型用法如下所示:

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

There are many ways this could be extended, for example by adding another member function in TupleConverter which returns a std::vector<std::string> of the names of the data members.有很多方法可以扩展它,例如通过在TupleConverter添加另一个成员函数,该函数返回数据成员名称的std::vector<std::string>

If I'd been allowed to use variadic macros then the solution might have been even better.如果我被允许使用可变参数宏,那么解决方案可能会更好。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM