tuple - c++17 descargar
cómo evitar un orden de ejecución no definido para los constructores cuando se usa std:: make_tuple (5)
¿Cómo puedo usar std :: make_tuple si el orden de ejecución de los constructores es importante?
Por ejemplo, supongo que el orden de ejecución del constructor de la clase A y el constructor de la clase B no está definido para:
std::tuple<A, B> t(std::make_tuple(A(std::cin), B(std::cin)));
Llegué a esa conclusión después de leer un comentario a la pregunta.
Traducir un std :: tuple en un paquete de parámetros de plantilla
eso dice que esto
template<typename... args>
std::tuple<args...> parse(std::istream &stream) {
return std::make_tuple(args(stream)...);
}
La implementación tiene un orden de ejecución indefinido de los constructores.
Actualización, proporcionando algún contexto:
Para dar más información sobre lo que estoy tratando de hacer, aquí hay un bosquejo:
Quiero leer algunos objetos serializados desde stdin con la ayuda del análisis / serialización binario CodeSynthesis XSD . Este es un ejemplo de cómo se realiza dicho análisis y serialización: example/cxx/tree/binary/xdr/driver.cxx
xml_schema::istream<XDR> ixdr (xdr);
std::auto_ptr<catalog> copy (new catalog (ixdr));
Quiero poder especificar una lista de las clases que tienen los objetos serializados (p. Ej., Catálogo, catálogo, alguna otra clase de serie serial para 3 objetos serializados) y almacenar esa información como un typedef
template <typename... Args>
struct variadic_typedef {};
typedef variadic_typedef<catalog, catalog, someOtherSerializableClass> myTypes;
como se sugiere en ¿Es posible "almacenar" un paquete de parámetros de plantilla sin expandirlo?
y encuentre la manera de obtener una std :: tuple para trabajar después de que el análisis haya finalizado. Un bosquejo:
auto serializedObjects(binaryParse<myTypes>(std::cin));
donde serializedObjects tendría el tipo
std::tuple<catalog, catalog, someOtherSerializableClass>
Como dice el comentario, puedes usar la lista de inicializadores:
return std::tuple<args...>{args(stream)...};
que funcionará para std::tuple
y suchlikes (que admite initializer-list).
Pero tengo otra solución que es más genérica y puede ser útil donde no se puede usar la lista de inicializadores. Así que resolvamos esto sin usar la lista de inicializadores:
template<typename... args>
std::tuple<args...> parse(std::istream &stream) {
return std::make_tuple(args(stream)...);
}
Antes de explicar mi solución, me gustaría discutir el problema primero. De hecho, pensar en el problema paso a paso también nos ayudaría a encontrar una solución con el tiempo. Entonces, para simplemente la discusión (y el proceso de pensamiento), asumamos que args
expande a 3 tipos distintos, a saber. X
, Y
, Z
, es decir, args = {X, Y, Z}
y luego podemos pensar en estas líneas, llegando a la solución paso a paso:
En primer lugar, los constructores de
X
,Y
yZ
pueden ejecutarse en cualquier orden, ya que el C ++ Standard no especifica el orden en que se evalúan los argumentos de la función.Pero queremos que
X
construya primero, luegoY
yZ
O al menos queremos simular ese comportamiento, lo que significa queX
debe construirse con datos que están al principio de la secuencia de entrada (digamos que los datos sonxData
) eY
debe construirse con datos que vienen inmediatamente después de xData, y así sucesivamente. .Como sabemos, no se garantiza que X se construya primero, así que tenemos que fingir . Básicamente, leeremos los datos del flujo como si estuvieran al principio del flujo, incluso si
Z
se construye primero, eso parece imposible. Es imposible siempre que leamos el flujo de entrada , pero leemos datos de alguna estructura de datos indexables comostd::vector
, entonces es posible.Entonces mi solución hace esto: primero poblará un
std::vector
, y luego todos los argumentos leerán los datos de este vector.Mi solución supone que cada línea de la ruta contiene todos los datos necesarios para construir un objeto de cualquier tipo.
Código:
//PARSE FUNCTION
template<typename... args>
std::tuple<args...> parse(std::istream &stream)
{
const int N = sizeof...(args);
return tuple_maker<args...>().make(stream, typename genseq<N>::type() );
}
Y tuple_maker
se define como:
//FRAMEWORK - HELPER ETC
template<int ...>
struct seq {};
template<int M, int ...N>
struct genseq : genseq<M-1,M-1, N...> {};
template<int ...N>
struct genseq<0,N...>
{
typedef seq<N...> type;
};
template<typename...args>
struct tuple_maker
{
template<int ...N>
std::tuple<args...> make(std::istream & stream, const seq<N...> &)
{
return std::make_tuple(args(read_arg<N>(stream))...);
}
std::vector<std::string> m_params;
std::vector<std::unique_ptr<std::stringstream>> m_streams;
template<int Index>
std::stringstream & read_arg(std::istream & stream)
{
if ( m_params.empty() )
{
std::string line;
while ( std::getline(stream, line) ) //read all at once!
{
m_params.push_back(line);
}
}
auto pstream = new std::stringstream(m_params.at(Index));
m_streams.push_back(std::unique_ptr<std::stringstream>(pstream));
return *pstream;
}
};
CÓDIGO DE PRUEBA
///TEST CODE
template<int N>
struct A
{
std::string data;
A(std::istream & stream)
{
stream >> data;
}
friend std::ostream& operator << (std::ostream & out, A<N> const & a)
{
return out << "A" << N << "::data = " << a.data ;
}
};
//three distinct classes!
typedef A<1> A1;
typedef A<2> A2;
typedef A<3> A3;
int main()
{
std::stringstream ss("A1/nA2/nA3/n");
auto tuple = parse<A1,A2,A3>(ss);
std::cout << std::get<0>(tuple) << std::endl;
std::cout << std::get<1>(tuple) << std::endl;
std::cout << std::get<2>(tuple) << std::endl;
}
Salida:
A1::data = A1
A2::data = A2
A3::data = A3
que se espera. Vea la demostración en ideone usted mismo. :-)
Tenga en cuenta que esta solución evita el problema del orden de lectura desde el flujo al leer todas las líneas en la primera llamada a read_arg
, y todas las llamadas posteriores solo se leen desde std::vector
, usando el índice.
Ahora puede poner un poco de printf en el constructor de las clases, solo para ver que el orden de construcción no es el mismo que el orden de los argumentos de plantilla a la plantilla de función de parse
, lo cual es interesante. Además, la técnica utilizada aquí puede ser útil para lugares donde no se puede utilizar la inicialización de lista.
Creo que la única forma de desenrollar manualmente la definición. Algo como lo siguiente podría funcionar. Aunque doy la bienvenida a los intentos de hacerlo más agradable.
#include <iostream>
#include <tuple>
struct A { A(std::istream& is) {}};
struct B { B(std::istream& is) {}};
template <typename... Ts>
class Parser
{ };
template <typename T>
class Parser<T>
{
public:
static std::tuple<T> parse(std::istream& is) {return std::make_tuple(T(is)); }
};
template <typename T, typename... Ts>
class Parser<T, Ts...>
{
public:
static std::tuple<T,Ts...> parse(std::istream& is)
{
A t(is);
return std::tuple_cat(std::tuple<T>(std::move(t)),
Parser<Ts...>::parse(is));
}
};
int main()
{
Parser<A,B>::parse(std::cin);
return 1;
}
Esta respuesta proviene de un comentario que hice a la pregunta del paquete de plantillas.
Dado que make_tuple
deduce el tipo de tupla de los componentes construidos y los argumentos de la función tienen un orden de evaluación indefinido, la construcción tiene que ocurrir dentro de la maquinaria, que es lo que propuse en el comentario. En ese caso, no es necesario usar make_tuple
; usted podría construir la tupla directamente del tipo de tupla. Pero eso tampoco ordena la construcción; lo que hago aquí es construir cada componente de la tupla, y luego construir una tupla de referencias a los componentes. La tupla de referencias se puede convertir fácilmente en una tupla del tipo deseado, siempre que los componentes sean fáciles de mover o copiar.
Aquí está la solución (del enlace de lws en el comentario) ligeramente modificada, y se explica un poco. Esta versión solo maneja tuplas cuyos tipos son todos diferentes, pero es más fácil de entender; Hay otra versión debajo que lo hace correctamente. Al igual que con el original, a todos los componentes de la tupla se les da el mismo argumento de constructor, pero el cambio simplemente requiere agregar un ...
a las líneas indicadas con // Note: ...
#include <tuple>
#include <type_traits>
template<typename...T> struct ConstructTuple {
// For convenience, the resulting tuple type
using type = std::tuple<T...>;
// And the tuple of references type
using ref_type = std::tuple<T&...>;
// Wrap each component in a struct which will be used to construct the component
// and hold its value.
template<typename U> struct Wrapper {
U value;
template<typename Arg>
Wrapper(Arg&& arg)
: value(std::forward<Arg>(arg)) {
}
};
// The implementation class derives from all of the Wrappers.
// C++ guarantees that base classes are constructed in order, and
// Wrappers are listed in the specified order because parameter packs don''t
// reorder.
struct Impl : Wrapper<T>... {
template<typename Arg> Impl(Arg&& arg) // Note ...Arg, ...arg
: Wrapper<T>(std::forward<Arg>(arg))... {}
};
template<typename Arg> ConstructTuple(Arg&& arg) // Note ...Arg, ...arg
: impl(std::forward<Arg>(arg)), // Note ...
value((static_cast<Wrapper<T>&>(impl)).value...) {
}
operator type() const { return value; }
ref_type operator()() const { return value; }
Impl impl;
ref_type value;
};
// Finally, a convenience alias in case we want to give `ConstructTuple`
// a tuple type instead of a list of types:
template<typename Tuple> struct ConstructFromTupleHelper;
template<typename...T> struct ConstructFromTupleHelper<std::tuple<T...>> {
using type = ConstructTuple<T...>;
};
template<typename Tuple>
using ConstructFromTuple = typename ConstructFromTupleHelper<Tuple>::type;
Vamos a dar una vuelta
#include <iostream>
// Three classes with constructors
struct Hello { char n; Hello(decltype(n) n) : n(n) { std::cout << "Hello, "; }; };
struct World { double n; World(decltype(n) n) : n(n) { std::cout << "world"; }; };
struct Bang { int n; Bang(decltype(n) n) : n(n) { std::cout << "!/n"; }; };
std::ostream& operator<<(std::ostream& out, const Hello& g) { return out << g.n; }
std::ostream& operator<<(std::ostream& out, const World& g) { return out << g.n; }
std::ostream& operator<<(std::ostream& out, const Bang& g) { return out << g.n; }
using std::get;
using Greeting = std::tuple<Hello, World, Bang>;
std::ostream& operator<<(std::ostream& out, const Greeting &n) {
return out << get<0>(n) << '' '' << get<1>(n) << '' '' << get<2>(n);
}
int main() {
// Constructors run in order
Greeting greet = ConstructFromTuple<Greeting>(33.14159);
// Now show the result
std::cout << greet << std::endl;
return 0;
}
liveworkspace en acción en liveworkspace . Verifique que se construya en el mismo orden tanto en clang como en gcc (la implementación de la tupla de libc ++ mantiene los componentes de la tupla en el orden inverso a stdlibc ++, así que supongo que es una prueba razonable).
Para que esto funcione con tuplas que pueden tener más de uno del mismo componente, es necesario modificar Wrapper
para que sea una estructura única para cada componente. La forma más sencilla de hacerlo es agregar un segundo parámetro de plantilla, que es un índice secuencial (tanto libc ++ como libstdc ++ hacen esto en sus implementaciones de tuplas; es una técnica estándar). Sería útil tener la implementación de "índices" dando vueltas para hacer esto, pero para propósitos de exposición, acabo de hacer una recursión rápida y sucia:
#include <tuple>
#include <type_traits>
template<typename T, int I> struct Item {
using type = T;
static const int value = I;
};
template<typename...TI> struct ConstructTupleI;
template<typename...T, int...I> struct ConstructTupleI<Item<T, I>...> {
using type = std::tuple<T...>;
using ref_type = std::tuple<T&...>;
// I is just to distinguish different wrappers from each other
template<typename U, int J> struct Wrapper {
U value;
template<typename Arg>
Wrapper(Arg&& arg)
: value(std::forward<Arg>(arg)) {
}
};
struct Impl : Wrapper<T, I>... {
template<typename Arg> Impl(Arg&& arg)
: Wrapper<T, I>(std::forward<Arg>(arg))... {}
};
template<typename Arg> ConstructTupleI(Arg&& arg)
: impl(std::forward<Arg>(arg)),
value((static_cast<Wrapper<T, I>&>(impl)).value...) {
}
operator type() const { return value; }
ref_type operator()() const { return value; }
Impl impl;
ref_type value;
};
template<typename...T> struct List{};
template<typename L, typename...T> struct WrapNum;
template<typename...TI> struct WrapNum<List<TI...>> {
using type = ConstructTupleI<TI...>;
};
template<typename...TI, typename T, typename...Rest>
struct WrapNum<List<TI...>, T, Rest...>
: WrapNum<List<TI..., Item<T, sizeof...(TI)>>, Rest...> {
};
// Use WrapNum to make ConstructTupleI from ConstructTuple
template<typename...T> using ConstructTuple = typename WrapNum<List<>, T...>::type;
// Finally, a convenience alias in case we want to give `ConstructTuple`
// a tuple type instead of a list of types:
template<typename Tuple> struct ConstructFromTupleHelper;
template<typename...T> struct ConstructFromTupleHelper<std::tuple<T...>> {
using type = ConstructTuple<T...>;
};
template<typename Tuple>
using ConstructFromTuple = typename ConstructFromTupleHelper<Tuple>::type;
Con prueba here .
La solución trivial no es usar std::make_tuple(...)
en primer lugar, sino construir un std::tuple<...>
directamente: el orden en el que se llama a los constructores para los miembros está bien definido:
template <typename>
std::istream& dummy(std::istream& in) {
return in;
}
template <typename... T>
std::tuple<T...> parse(std::istream& in) {
return std::tuple<T...>(dummy<T>(in)...);
}
La plantilla de función dummy<T>()
solo se usa para tener algo que expandir. El orden se impone por orden de construcción de los elementos en el std::tuple<T...>
:
template <typename... T>
template <typename... U>
std::tuple<T...>::tuple(U...&& arg)
: members_(std::forward<U>(arg)...) { // NOTE: pseudo code - the real code is
} // somewhat more complex
Siguiendo la discusión a continuación y el comentario de Xeo, parece que una mejor alternativa es usar
template <typename... T>
std::tuple<T...> parse(std::istream& in) {
return std::tuple<T...>{ T(in)... };
}
El uso de la inicialización de llaves funciona porque el orden de evaluación de los argumentos en una lista de inicializadores de llaves es el orden en que aparecen. La semántica de T{...}
se describe en 12.6.1 [class.explicit.init] párrafo 2 que indica que sigue las reglas de la semántica de inicialización de lista (nota: esto no tiene nada que ver con std :: initializer_list que solo funciona con tipos homogéneos). La restricción de orden se encuentra en 8.5.4 [dcl.init.list] párrafo 4.
No hay nada especial acerca de make_tuple
aquí. Cualquier llamada a la función en C ++ permite que sus argumentos sean llamados en un orden no especificado (permitiendo que el compilador se optimice).
Realmente no sugiero tener constructores que tengan efectos secundarios tales que el orden sea importante (esto será una pesadilla de mantenimiento), pero si es absolutamente necesario, siempre puede construir los objetos explícitamente para establecer el orden que desee:
A a(std::cin);
std::tuple<A, B> t(std::make_tuple(a, B(std::cin)));