С++ 11 Tagged Tuple

С++ 11 кортежей хороши, но у них есть два худших недостатка, доступ к членам по индексу

  1. нечитаемый
  2. difficilt для поддержания (если я добавлю элемент в середине кортежа, я прикручен)

В сущности, я хочу достичь этого

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;

Нечто похожее (тип tagging) реализуется в boost :: property_map, но я не понимаю, как реализовать его в кортеже с арбированным числом элементов

PS Пожалуйста, не предлагайте определение перечисления с помощью индексов элементов кортежа.

UPD ОК, вот мотивация. В моих проектах мне нужно иметь возможность определять множество разных кортежей "на лету", и все они должны иметь определенные общие функции и операторы. Это невозможно достичь с помощью структур

UPD2 На самом деле мой пример, вероятно, немного нереалистичен для реализации. Как насчет этого?

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;

Ответ 1

Я не знаю ни одного существующего класса, который делает это, но довольно легко сбрасывать что-то вместе, используя 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, "[email protected]"};
}

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

Вероятно, вы захотите написать const и rvalue get accessors поверх существующего.

Ответ 2

C++ не имеет типа struct который может быть итерационным, как tuple; это либо/или.

Ближе всего вы можете добраться до этого через адаптер структуры Boost.Fusion. Это позволяет использовать структуру как последовательность Fusion. Конечно, это также использует ряд макросов, и для этого требуется, чтобы вы явно перечисляли элементы структуры в том порядке, в котором вы хотите перебирать их. В заголовке (если вы хотите перебрать структуру во многих единицах перевода).

На самом деле мой пример, вероятно, немного нереалистичен для реализации. Как насчет этого?

Вы можете реализовать что-то подобное, но эти идентификаторы действительно должны быть типами или переменными или чем-то еще.

Ответ 3

У меня есть моя собственная реализация, чтобы показать, что позволяет вам не объявлять атрибуты поверх файла. Также существует версия с объявленными атрибутами, но нет необходимости их определять, объявление достаточно.

Конечно, это чистый 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. Не стесняйтесь читать, это довольно просто.

Ответ 4

Я реализовал "c++ с именем tuple" с использованием препроцессора boost. См. Пример использования ниже. Вывод из кортежа, я получаю сравнение, печать, хэш, сериализацию бесплатно (при условии, что они определены для кортежа).

#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.

Ответ 5

Реальные проблемы, которые вы должны решить здесь:

  • Являются ли теги обязательными или необязательными?
  • Являются ли теги уникальными? Это принудительно во время компиляции?
  • В какой области находится тег? Ваш пример, кажется, объявляет теги внутри области объявления, а не инкапсулируется в типе, что может быть не оптимальным.

ecatmur предложил хорошее решение; но теги не инкапсулированы, а объявление тега каким-то неуклюжим. c++ 14 будет вводить адресацию по типу, что упростит его дизайн и гарантирует уникальность тегов, но не решит их объем.

Boost Fusion Map также можно использовать для чего-то подобного, но опять же, объявление тегов не является идеальным.

На форуме c++ Standard Proposal есть предложение о чем-то подобном, что упростит синтаксис, напрямую связав имя с параметром шаблона.

В этой ссылке перечислены различные способы ее реализации (включая решение ecatmur) и представлены различные варианты использования этого синтаксиса.

Ответ 6

Я "решил" аналогичную проблему в производственном коде. Во-первых, у меня есть обычная структура (фактически класс с различными функциями-членами, но это только те элементы данных, которые нас интересуют здесь)...

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. Переменная public const overload возвращает кортеж ссылок на 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> имен членов данных.

Если бы мне разрешили использовать переменные макросы, тогда решение могло быть еще лучше.

Ответ 7

Здесь еще один способ сделать это, это немного уродливее, чтобы определить типы, но это помогает предотвратить ошибки во время компиляции, потому что вы определяете пары с классом type_pair (так же, как std::map). Добавление проверки, чтобы убедиться, что ваши ключи/имя уникальны во время компиляции, - это следующий шаг

Применение:

   using user_t = tagged_tuple<type_pair<struct name, std::string>, type_pair<struct age, int>>;
  // it 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;

Я отказался от того, чтобы получить функцию-член, чтобы он был максимально похож на std :: tuple, но вы можете легко добавить его в класс. Исходный код здесь

Ответ 8

Вот реализация, подобная ответу ecatmur с использованием библиотеки метапрограммирования разбойников (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;
}