40

C++11 元组很好,但它们对我有两个巨大的缺点,按索引访问成员是

  1. 不可读
  2. 难以维护(如果我在元组中间添加一个元素,我就完蛋了)

本质上我想要实现的是这个

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;

在 boost::property_map 中实现了类似的东西(类型标记),但我无法理解如何在具有任意数量元素的元组中实现它

PS 请不要建议使用元组元素索引定义枚举。

UPD 好的,这是一个动机。在我的项目中,我需要能够“即时”定义许多不同的元组,并且它们都需要具有某些通用函数和运算符。这是结构无法实现的

UPD2 实际上,我的示例实施起来可能有点不切实际。这个怎么样?

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

8 回答 8

54

我不知道有任何现有的类可以做到这一点,但是使用 astd::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;
}

您可能希望在现有访问器之上编写const和右值访问器。get

于 2012-10-25T09:58:55.307 回答
10

C++ 没有像;struct这样可以迭代的类型。tuple这是非此即彼的。

最接近的方法是通过 Boost.Fusion 的struct adapter。这允许您将结构用作融合序列。当然,这也使用了一系列宏,并且它要求您按照要迭代它们的顺序显式列出结构的成员。在标题中(假设您想在许多翻译单元中迭代结构)。

实际上,我的示例实施起来可能有点不切实际。这个怎么样?

您可以实现类似的东西,但这些标识符实际上需要是类型或变量或其他东西。

于 2012-10-25T09:52:45.297 回答
8

我有我自己的实现来炫耀,它可以让你不要在文件顶部声明属性。也存在声明属性的版本,但不需要定义它们,声明就足够了。

当然,它是纯 STL,并且不使用预处理器。

例子:

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

在这里找到完整的源代码https://github.com/duckie/named_tuple。随意阅读,很简单。

于 2014-05-13T03:56:44.620 回答
2

您必须在这里解决的真正问题是:

  • 标签是强制性的还是可选的?
  • 标签是唯一的吗?它是在编译时强制执行的吗?
  • 标签驻留在哪个范围内?您的示例似乎在声明范围内声明了标签,而不是封装在类型中,这可能不是最佳的。

ecatmur提出了一个很好的解决方案;但是标签没有封装,标签声明有点笨拙。C++14 将引入按类型寻址的元组,这将简化他的设计并保证标签的唯一性,但不能解决它们的范围。

Boost Fusion Map也可以用于类似的事情,但同样,声明标签并不理想。

在c++ Standard Proposal forum上有一个类似的提案,它可以通过将名称直接关联到模板参数来简化语法。

此链接列出了实现此功能的不同方法(包括ecatmur的解决方案),并为此语法提供了不同的用例。

于 2014-04-10T13:55:48.840 回答
1

这是另一种方法,定义类型有点丑陋,但它有助于防止编译时出错,因为您使用type_pair类定义了对(很像std::map)。下一步是添加检查以确保您的密钥/名称在编译时是唯一的

用法:

   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;

我选择不让 get 成为成员函数,以使其尽可能与 std::tuple 相似,但您可以轻松地将一个添加到类中。 源代码在这里

于 2015-02-10T05:45:36.590 回答
1

这是使用 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;
}
于 2017-03-23T23:36:24.327 回答
0

我使用 boost 预处理器实现了“c++ named 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_;  
// };

请注意,上面的代码示例假设您已经为 vector/tuple/unordered_set 定义了“通用”ostream 打印函数。

于 2013-06-12T18:00:20.367 回答
0

我已经“解决”了生产代码中的类似问题。首先,我有一个普通的结构(实际上是一个具有各种成员函数的类,但它只是我们这里感兴趣的数据成员)......

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

然后在结构定义下方,但在任何命名空间之外,我有另一个宏:

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

这种方法的缺点是成员名称必须列出两次,但这是我能想到的最好的方法,同时仍然允许结构自己的成员函数中的传统成员访问语法。宏出现在数据成员本身的定义附近,因此保持它们彼此同步并不难。

在另一个头文件中,我有一个类模板:

template <class T>
class TupleConverter;

定义第一个宏是为了将这个模板声明为friend结构的一个,因此它可以访问它的私有数据成员:

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

定义第二个宏是为了引入模板的特化:

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

这会创建相同成员函数名称的两个重载,一个TupleConverter<Record>::toTuple(Record const&)是公共的,TupleConverter<Record>::toTuple(Record&)一个是私有的,只能Record通过友谊访问。两者都返回他们的参数,通过std::tie. 公共 const 重载返回一个对 const 的引用元组,私有非常量重载返回一个对非常量引用的元组。

在预处理器替换之后,两个friend声明都引用了在同一个头文件中定义的实体,因此其他代码应该没有机会滥用友谊来破坏封装。

toTuple不能是 的成员函数Record,因为在 的定义完成之前无法推导出它的返回类型Record

典型用法如下所示:

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

有很多方法可以扩展它,例如通过添加另一个成员函数,TupleConverter其中返回std::vector<std::string>数据成员的名称。

如果我被允许使用可变参数宏,那么解决方案可能会更好。

于 2014-04-10T15:30:38.190 回答