cómo evitar el orden de ejecución indefinido para los constructores al usar std :: make_tuple

¿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 clase A y el constructor de clase B no está definido para:

std::tuple 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 una std :: tuple en un paquete de parámetros de plantilla

eso dice que esto

 template std::tuple 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 boceto:

Quiero leer en algunos objetos serializados de stdin con la ayuda de CodeSynthesis XSD binary parsing / serializing. Aquí hay un ejemplo de cómo se realiza dicho análisis sintáctico y serialización: example / cxx / tree / binary / xdr / driver.cxx

 xml_schema::istream ixdr (xdr); std::auto_ptr copy (new catalog (ixdr)); 

Quiero poder especificar una lista de las clases que tienen los objetos seriados (por ejemplo, catálogo, catálogo, algunos otros SerializableClass para 3 objetos serializados) y almacenar esa información como un typedef

 template  struct variadic_typedef {}; typedef variadic_typedef myTypes; 

como se sugiere en ¿Es posible “almacenar” un paquete de parámetros de plantilla sin expandirlo?

y encuentre una manera de hacer que una std :: tuple trabaje después de que el análisis haya finalizado. Un bosquejo:

 auto serializedObjects(binaryParse(std::cin)); 

donde serializedObjects tendría el tipo

 std::tuple 

La solución trivial es no usar std::make_tuple(...) en primer lugar, sino construir un std::tuple<...> directamente: el orden en que se llaman los constructores para los miembros está bien definido:

 template  std::istream& dummy(std::istream& in) { return in; } template  std::tuple parse(std::istream& in) { return std::tuple(dummy(in)...); } 

La plantilla de función dummy() solo se usa para expandir algo. El orden se impone por orden de construcción de los elementos en std::tuple :

 template  template  std::tuple::tuple(U...&& arg) : members_(std::forward(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  std::tuple parse(std::istream& in) { return std::tuple{ 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, indicando que sigue las reglas de 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 ordenamiento está en 8.5.4 [dcl.init.list], párrafo 4.

Como dice el comentario, puedes usar initializer-list:

 return std::tuple{args(stream)...}; 

que funcionará para std::tuple y suchlikes (que admite initializer-list).

Pero obtuve otra solución que es más genérica y puede ser útil cuando no se puede usar la lista de inicializadores. Así que vamos a resolver esto sin usar initializer-list:

 template std::tuple 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), supongamos que args expande a 3 tipos distintos, viz. X , Y , Z , es decir, args = {X, Y, Z} y luego podemos pensar en esta línea, alcanzando la solución paso a paso:

  • En primer lugar, los constructores de X , Y y Z se pueden ejecutar en cualquier orden, porque el orden en el que se evalúan los argumentos de la función no está especificado por el estándar de C ++.

  • Pero queremos que X construya primero, luego Y y Z O al menos queremos simular ese comportamiento, lo que significa que X debe construirse con datos que están al principio de la stream de entrada (digamos que los datos son xData ) e Y debe construirse con datos que vienen inmediatamente después de xData, y así sucesivamente .

  • Como sabemos, X no está garantizado para ser construido primero, por lo que debemos fingir . Básicamente, leeremos los datos de la secuencia como si estuvieran al principio de la secuencia, incluso si Z se construye primero, eso parece imposible. Es imposible siempre y cuando leamos desde el flujo de entrada , pero leemos datos de una estructura de datos indexable como std::vector , entonces es posible.

  • Entonces mi solución hace esto: poblará primero un std::vector , y luego todos los argumentos leerán datos de este vector.

  • Mi solución asume que cada línea en la secuencia contiene todos los datos necesarios para construir un objeto de cualquier tipo.

Código:

 //PARSE FUNCTION template std::tuple parse(std::istream &stream) { const int N = sizeof...(args); return tuple_maker().make(stream, typename genseq::type() ); } 

Y tuple_maker se define como:

 //FRAMEWORK - HELPER ETC template struct seq {}; template struct genseq : genseq {}; template struct genseq<0,N...> { typedef seq type; }; template struct tuple_maker { template std::tuple make(std::istream & stream, const seq &) { return std::make_tuple(args(read_arg(stream))...); } std::vector m_params; std::vector> m_streams; template 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(pstream)); return *pstream; } }; 

CÓDIGO DE PRUEBA

 ///TEST CODE template struct A { std::string data; A(std::istream & stream) { stream >> data; } friend std::ostream& operator << (std::ostream & out, A 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(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 de la secuencia al leer todas las líneas en la primera llamada a read_arg y todas las llamadas posteriores recién leídas de std::vector , utilizando el índice.

Ahora puede poner 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 la plantilla para 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 usar la inicialización de la lista.

No hay nada especial sobre make_tuple aquí. Cualquier llamada a función en C ++ permite que se invoquen sus argumentos en un orden no especificado (lo que permite que la libertad del comstackdor 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 realmente lo necesitas, siempre puedes construir los objetos explícitamente para establecer el orden que deseas:

 A a(std::cin); std::tuple t(std::make_tuple(a, B(std::cin))); 

Esta respuesta proviene de un comentario que hice a la pregunta del paquete de plantillas

Como make_tuple deduce el tipo de tupla de los componentes construidos y los argumentos de función tienen un ordenador 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 ; podrías construir la tupla directamente desde el 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 lws en el comentario) ligeramente modificada, y explicada 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 los componentes de la tupla se les da el mismo argumento de constructor, pero cambiar eso simplemente requiere agregar un ... a las líneas indicadas con // Note: ...

 #include  #include  template struct ConstructTuple { // For convenience, the resulting tuple type using type = std::tuple; // And the tuple of references type using ref_type = std::tuple; // Wrap each component in a struct which will be used to construct the component // and hold its value. template struct Wrapper { U value; template Wrapper(Arg&& arg) : value(std::forward(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... { template Impl(Arg&& arg) // Note ...Arg, ...arg : Wrapper(std::forward(arg))... {} }; template ConstructTuple(Arg&& arg) // Note ...Arg, ...arg : impl(std::forward(arg)), // Note ... value((static_cast&>(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 struct ConstructFromTupleHelper; template struct ConstructFromTupleHelper> { using type = ConstructTuple; }; template using ConstructFromTuple = typename ConstructFromTupleHelper::type; 

Vamos a dar un giro

 #include  // 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 << gn; } std::ostream& operator<<(std::ostream& out, const World& g) { return out << gn; } std::ostream& operator<<(std::ostream& out, const Bang& g) { return out << gn; } using std::get; using Greeting = std::tuple; 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(33.14159); // Now show the result std::cout << greet << std::endl; return 0; } 

Véalo en acción en liveworkspace . Verifique que se construye en el mismo orden tanto en clang como en gcc (la implementación tuple de libc ++ contiene los componentes de la tupla en el orden inverso a stdlibc ++, por lo que es una prueba razonable, supongo).

Para hacer 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 fines de exposición, acabo de hacer una recursión rápida y sucia:

 #include  #include  template struct Item { using type = T; static const int value = I; }; template struct ConstructTupleI; template struct ConstructTupleI...> { using type = std::tuple; using ref_type = std::tuple; // I is just to distinguish different wrappers from each other template struct Wrapper { U value; template Wrapper(Arg&& arg) : value(std::forward(arg)) { } }; struct Impl : Wrapper... { template Impl(Arg&& arg) : Wrapper(std::forward(arg))... {} }; template ConstructTupleI(Arg&& arg) : impl(std::forward(arg)), value((static_cast&>(impl)).value...) { } operator type() const { return value; } ref_type operator()() const { return value; } Impl impl; ref_type value; }; template struct List{}; template struct WrapNum; template struct WrapNum> { using type = ConstructTupleI; }; template struct WrapNum, T, Rest...> : WrapNum>, Rest...> { }; // Use WrapNum to make ConstructTupleI from ConstructTuple template using ConstructTuple = typename WrapNum, T...>::type; // Finally, a convenience alias in case we want to give `ConstructTuple` // a tuple type instead of a list of types: template struct ConstructFromTupleHelper; template struct ConstructFromTupleHelper> { using type = ConstructTuple; }; template using ConstructFromTuple = typename ConstructFromTupleHelper::type; 

Con prueba aquí .

Creo que la única forma de desenrollar manualmente la definición. Algo como lo siguiente podría funcionar. Sin embargo, doy la bienvenida a los bashs de hacerlo más agradable.

 #include  #include  struct A { A(std::istream& is) {}}; struct B { B(std::istream& is) {}}; template  class Parser { }; template  class Parser { public: static std::tuple parse(std::istream& is) {return std::make_tuple(T(is)); } }; template  class Parser { public: static std::tuple parse(std::istream& is) { A t(is); return std::tuple_cat(std::tuple(std::move(t)), Parser::parse(is)); } }; int main() { Parser::parse(std::cin); return 1; }