make_tuple - tuple c++ example
C++ 11 tuple etiquetado (8)
Las tuplas de C ++ 11 son agradables, pero tienen dos desventajas de hude para mí: acceder a los miembros por índice es
- ilegible
- Difícil de mantener (si agrego un elemento en el medio de la tupla, estoy atornillado)
En esencia lo que quiero lograr es esto.
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;
Algo similar (etiquetado de tipo) se implementa en boost :: property_map, pero no puedo entender cómo implementarlo en una tupla con un número arbitrario de elementos.
PS Por favor, no sugiera definir una enumeración con elementos de tupla indecies.
UPD OK, aquí hay una motivación. En mis proyectos necesito poder definir muchas tuplas diferentes ''sobre la marcha'' y todas ellas deben tener ciertas funciones y operadores comunes. Esto no es posible lograr con estructuras.
UPD2 En realidad, mi ejemplo es probablemente poco realista de implementar. ¿Qué tal esto?
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;
Aquí hay otra forma de hacerlo, es un poco más feo definir los tipos, pero ayuda a evitar errores en el momento de la compilación porque define los pares con una clase type_pair
(como std::map
). Agregar un cheque para asegurarse de que sus claves / nombre sean únicos en el momento de la compilación es el siguiente paso
Uso:
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;
Opté por no ser miembro de una función para mantenerla tan similar a std :: tuple como sea posible, pero fácilmente podría agregar una a la clase. Código fuente aquí
Aquí hay una implementación similar a la respuesta de ecatmur utilizando la biblioteca de metaprogramación de 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;
}
C ++ no tiene un tipo de struct
que pueda ser iterable como una tuple
; es o o
Lo más cerca que puede llegar a eso es a través del adaptador de estructura de Boost.Fusion. Esto le permite utilizar una estructura como una secuencia de Fusion. Por supuesto, esto también utiliza una serie de macros, y requiere que enumere los miembros de la estructura explícitamente en el orden que desee iterar sobre ellos. En el encabezado (suponiendo que desea iterar sobre la estructura en muchas unidades de traducción).
En realidad, mi ejemplo es probablemente poco realista de implementar. ¿Qué tal esto?
Podría implementar algo así, pero esos identificadores deben ser tipos o variables o algo así.
He "resuelto" un problema similar en el código de producción. Primero, tengo una estructura ordinaria (en realidad, una clase con varias funciones de miembro, pero son los miembros de datos lo que nos interesa aquí) ...
class Record
{
std::string name;
int age;
std::string email;
MYLIB_ENABLE_TUPLE(Record) // macro
};
Luego, justo debajo de la definición de la estructura, pero fuera de cualquier espacio de nombres, tengo otra macro:
MYLIB_DECLARE_TUPLE(Record, (o.name, o.age, o.email))
La desventaja de este enfoque es que los nombres de los miembros deben aparecer dos veces en la lista, pero este es el mejor que he podido encontrar al mismo tiempo que permite la sintaxis tradicional de acceso de los miembros dentro de las propias funciones miembro de la estructura. La macro aparece muy cerca de las definiciones de los propios miembros de los datos, por lo que no es demasiado difícil mantenerlos sincronizados entre sí.
En otro archivo de cabecera tengo una plantilla de clase:
template <class T>
class TupleConverter;
La primera macro se define para declarar que esta plantilla es friend
de la estructura, para que pueda acceder a sus miembros de datos privados:
#define MYLIB_ENABLE_TUPLE(TYPE) friend class TupleConverter<TYPE>;
La segunda macro se define para introducir una especialización de la plantilla:
#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; /
} /
};
Esto crea dos sobrecargas del mismo nombre de función miembro, TupleConverter<Record>::toTuple(Record const&)
que es público, y TupleConverter<Record>::toTuple(Record&)
que es privado y accesible solo para Record
a través de la amistad. Ambos devuelven su argumento convertido en una tupla de referencias a miembros de datos privados a través de std::tie
. La sobrecarga de const pública devuelve una tupla de referencias a const, la sobrecarga privada no const devuelve una tupla de referencias a no const.
Después de la sustitución del preprocesador, ambas declaraciones de friend
refieren a entidades definidas en el mismo archivo de encabezado, por lo que no debería haber ninguna posibilidad de que otros códigos abusen de la amistad para romper la encapsulación.
toTuple
no puede ser una función miembro de Record
, porque su tipo de retorno no se puede deducir hasta que se complete la definición de Record
.
El uso típico se ve así:
// 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);
}
Hay muchas maneras de ampliarlo, por ejemplo, agregando otra función miembro en TupleConverter
que devuelve un std::vector<std::string>
de los nombres de los miembros de los datos.
Si me hubieran permitido usar macros variables, entonces la solución podría haber sido incluso mejor.
Implementé "c ++ named tuple" utilizando el preprocesador boost. Por favor, consulte el uso de la muestra a continuación. Al derivar de la tupla, obtengo comparación, impresión, hash, serialización gratuita (asumiendo que están definidas para tupla).
#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_;
// };
Tenga en cuenta que el ejemplo de código anterior asume que ha definido funciones de impresión ostream "genéricas" para vector / tuple / unordered_set.
Los problemas reales que tienes que resolver aquí son:
- ¿Las etiquetas son obligatorias u opcionales?
- ¿Son las etiquetas únicas? ¿Se aplica en tiempo de compilación?
- ¿En qué ámbito reside la etiqueta? Su ejemplo parece declarar las etiquetas dentro del ámbito de declaración en lugar de encapsularse en el tipo, lo que podría no ser óptimo.
ecatmur propuso una buena solución; pero las etiquetas no están encapsuladas y la declaración de la etiqueta es algo torpe. C ++ 14 introducirá el direccionamiento de la tupla por tipo , lo que simplificará su diseño y garantizará la singularidad de las etiquetas, pero no resolverá su alcance.
Boost Fusion Map también se puede usar para algo similar, pero nuevamente, declarar que las etiquetas no son ideales.
Hay una propuesta para algo similar en el foro de propuestas estándar de c ++ , que simplificaría la sintaxis asociando un nombre al parámetro de la plantilla directamente.
Este enlace enumera diferentes formas de implementar esto (incluida la solución de ecatmur ) y presenta un caso de uso diferente para esta sintaxis.
No estoy al tanto de ninguna clase existente que haga esto, pero es bastante fácil juntar algo usando un std::tuple
y una lista de tipos de indexación:
#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;
}
Probablemente querrá escribir const
y rvalue get
accessors encima del existente.
Tengo mi propia implementación para mostrar, lo que puede permitirte no declarar los atributos sobre el archivo. También existe una versión con atributos declarados, pero no es necesario definirlos, la declaración es suficiente.
Es puro STL, por supuesto, y no utiliza el preprocesador.
Ejemplo:
#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;
}
Encuentre la fuente completa aquí https://github.com/duckie/named_tuple . Siéntase libre de leer, es bastante simple.