swirl prioridad orden operador_binario lección funciones fun evaluación c++ templates function-templates templates-deduction partial-ordering

c++ - prioridad - Orden parcial con plantilla de función que tiene contexto sin deducir



operador_binario<- function(fun, a, b){ (4)

Lista de parámetros transformada para T1 (Q insertado): (Q, typename Const :: type *). Los tipos de argumentos son AT = (Q, void *)

Me pregunto si realmente es una simplificación correcta. Cuando sintetizas el tipo Q , ¿puedes invocar una especialización para Const para determinar el orden de la especificación de la plantilla?

template <> struct Const<Q> { typedef int type; }

Esto implicaría que T2 no es al menos tan especializado como T1 porque un parámetro void* no coincide con el segundo parámetro de T1 para ningún parámetro de plantilla dado.

Mientras leía otra pregunta, llegué a un problema con el pedido parcial, que reduje al siguiente caso de prueba

template<typename T> struct Const { typedef void type; }; template<typename T> void f(T, typename Const<T>::type*) { cout << "Const"; } // T1 template<typename T> void f(T, void*) { cout << "void*"; } // T2 int main() { // GCC chokes on f(0, 0) (not being able to match against T1) void *p = 0; f(0, p); }

Para ambas plantillas de funciones, el tipo de función de la especialización que ingresa la resolución de sobrecarga es void(int, void*) . Pero el ordenamiento parcial (de acuerdo con Comeau y GCC) ahora dice que la segunda plantilla es más especializada. ¿Pero por qué?

Déjame ir a través de pedidos parciales y mostrar dónde tengo preguntas. Puede Q ser un tipo único de 14.5.5.2 utilizado para determinar el orden parcial de acuerdo con 14.5.5.2 .

  • Lista de parámetros transformada para T1 (Q insertada): (Q, typename Const<Q>::type*) . Los tipos de argumentos son AT = (Q, void*)
  • Lista de parámetros transformados para T2 (Q insertado): BT = (Q, void*) , que también son los tipos de los argumentos.
  • Lista de parámetros no transformados para T1 : (T, typename Const<T>::type*)
  • Lista de parámetros no transformados para T2 : (T, void*)

Como C ++ 03 no especifica esto, utilicé la intención que leí en varios informes de defectos. La lista de parámetros transformados anterior para T1 (llamada AT por mí) se utiliza como lista de argumentos para 14.8.2.1 "Deducir argumentos de plantilla desde una llamada a función" .

14.8.2.1 no necesita transformar AT o BT sí mismo (como eliminar los declaradores de referencia, etc.), y va directamente a 14.8.2.4 , que independientemente para cada par de A / P escribe la deducción:

  • AT contra T2 : { (Q, T) , (void*, void*) } . T es el único parámetro de plantilla aquí, y encontrará que T debe ser Q La deducción del tipo tiene éxito trivial para AT contra T2 .

  • BT contra T1 : { (Q, T) , (void*, typename Const<T>::type*) } . Encontrará que T es Q , también aquí. typename Const<T>::type* es un contexto no deducido, por lo que no se usará para deducir nada.

Aquí está mi primera pregunta: ¿Esto usará ahora el valor de T deducido para el primer parámetro? Si la respuesta es no, entonces la primera plantilla es más especializada. Esto no puede ser el caso, porque tanto GCC como Comeau dicen que la segunda plantilla es más especializada, y no creo que estén equivocados. Entonces asumimos "sí", e insertamos el void* en T El párrafo ( 14.8.2.4 ) dice "La deducción se realiza de forma independiente para cada par y los resultados se combinan" y también "En ciertos contextos, sin embargo, el valor no participa en la deducción de tipo, sino que usa los valores de argumentos de plantilla que fueron deducidos en otra parte o explícitamente especificados ". Esto suena como "sí" también.

La deducción también tiene éxito, por cada par de A / P. Ahora, cada plantilla es al menos tan especializada como la otra, porque la deducción tampoco se basó en conversiones implícitas y tuvo éxito en ambas direcciones. Como resultado, la llamada debe ser ambigua.

Entonces mi segunda pregunta: ahora, ¿por qué las implementaciones dicen que la segunda plantilla es más especializada? ¿Qué punto pasé por alto?

Editar : Probé especialización e instanciación explícitas, y ambas, en versiones recientes de GCC ( 4.4 ), me dicen que la referencia a la especialización es ambigua, mientras que una versión anterior de GCC ( 4.1 ) no aumenta ese error de ambigüedad. Esto sugiere que las versiones recientes de GCC tienen un orden parcial inconsistente para las plantillas de funciones.

template<typename T> struct Const { typedef void type; }; template<typename T> void f(T, typename Const<T>::type*) { cout << "Const"; } // T1 template<typename T> void f(T, void*) { cout << "void*"; } // T2 template<> void f(int, void*) { } // main.cpp:11: error: ambiguous template specialization // ''f<>'' for ''void f(int, void*)''


Aquí está mi ir a esto. Estoy de acuerdo con Charles Bailey en que el paso incorrecto es pasar de Const<Q>::Type* a void*

template<typename T> void f(T, typename Const<T>::type*) { cout << "Const"; } // T1 template<typename T> void f(T, void*) { cout << "void*"; } // T2

Los pasos que queremos dar son:

14.5.5.2/2

Dadas dos plantillas de funciones sobrecargadas, se puede determinar si una es más especializada que otra transformando cada plantilla por turno y usando la deducción de argumentos (14.8.2) para compararla con la otra.

14.5.5.2/3-b1

Para cada parámetro de plantilla de tipo, sintetice un tipo único y sustitúyalo por cada aparición de ese parámetro en la lista de parámetros de función, o para una función de conversión de plantilla, en el tipo de devolución.

En mi opinión, los tipos se sintetizan de la siguiente manera:

(Q, Const<Q>::Type*) // Q1 (Q, void*) // Q2

No veo ninguna fraseología que requiera que el segundo parámetro sintetizado de T1 sea void* . Tampoco conozco ningún precedente para eso en otros contextos. El tipo Const<Q>::Type* es un tipo perfectamente válido dentro del sistema de tipo C ++.

Entonces ahora realizamos los pasos de deducción:

Q2 a T1

Tratamos de deducir los parámetros de la plantilla para T1, así que tenemos:

  • Parámetro 1: T se deduce para ser Q
  • Parámetro 2: contexto no deducido

Aunque el parámetro 2 es un contexto no deducido, la deducción todavía ha tenido éxito porque tenemos un valor para T.

Q1 a T2

Deduciendo los parámetros de plantilla para T2 tenemos:

  • Parámetro 1: T se deduce para ser Q
  • Parámetro 2: void* no coincide con Const<Q>::Type* por lo que falla la deducción.

En mi humilde opinión, aquí es donde el estándar nos decepciona. El parámetro no es dependiente, por lo que no está claro lo que debería suceder, sin embargo, mi experiencia (basada en una lectura entrecortada de 14.8.2.1/3) es que incluso cuando el parámetro tipo P no es dependiente, entonces el argumento tipo A debe coincidir eso.

Los argumentos sintetizados de T1 se pueden usar para especializar T2, pero no al revés. T2 es por lo tanto más especializado que T1 y, por lo tanto, es la mejor función.

ACTUALIZACIÓN 1:

Solo para cubrir el poing sobre el tipo de Const<Q>::type siendo nulo. Considere el siguiente ejemplo:

template<typename T> struct Const; template<typename T> void f(T, typename Const<T>::type*) // T1 { typedef typename T::TYPE1 TYPE; } template<typename T> void f(T, void*) // T2 { typedef typename T::TYPE2 TYPE ; } template<> struct Const <int> { typedef void type; }; template<> struct Const <long> { typedef long type; }; void bar () { void * p = 0; f (0, p); }

En lo anterior, Const<int>::type se usa cuando estamos realizando las reglas de resolución de sobrecarga habituales, pero no cuando llegamos a las reglas de sobrecarga parcial. No sería correcto elegir una especialización arbitraria para Const<Q>::type . Puede que no sea intuitivo, pero el compilador está bastante contento de tener un tipo sintetizado de la forma Const<Q>::type* y usarlo durante la deducción del tipo.

ACTUALIZACIÓN 2

template <typename T, int I> class Const { public: typedef typename Const<T, I-1>::type type; }; template <typename T> class Const <T, 0> { public: typedef void type; }; template<typename T, int I> void f(T (&)[I], typename Const<T, I>::type*) // T1 { typedef typename T::TYPE1 TYPE; } template<typename T, int I> void f(T (&)[I], void*) // T2 { typedef typename T::TYPE2 TYPE ; } void bar () { int array[10]; void * p = 0; f (array, p); }

Cuando la plantilla Const se instancia con algún valor I , se ejemplifica recursivamente hasta I llegue a 0. Aquí es cuando se selecciona la especialización parcial Const<T,0> . Si tenemos un compilador que sintetiza algún tipo real para los parámetros de la función, ¿qué valor elegirá el compilador para el índice de matriz? Diga 10? Bueno, esto estaría bien para el ejemplo anterior, pero no coincidiría con la especialización parcial Const<T, 10 + 1> que, al menos conceptualmente, daría como resultado un número infinito de instancias recursivas del primario. Cualquiera sea el valor que seleccione, podríamos modificar la condición final para que sea ese valor + 1, y luego tendríamos un ciclo infinito en el algoritmo de ordenamiento parcial.

No veo cómo el algoritmo de ordenamiento parcial podría crear una instancia correcta de Const para encontrar qué type realmente es.


Editar: Después de estudiar la implementación de Clang (por Doug Gregor) de su algoritmo de ordenamiento parcial, he llegado a un acuerdo con el resto de los carteles que el ejemplo original no es ''ambiguo'' -aunque el estándar no es tan claro como podría tratarse de lo que debería suceder en tales situaciones. He editado esta publicación para indicar mis pensamientos revisados ​​(para mi propio beneficio y referencia). En particular, el algoritmo de Clang aclaró que '' typename Const<T>::type '' no se traduce en ''void'' durante el paso de ordenamiento parcial, y que cada par de A / P se deduce de forma independiente el uno del otro.

Inicialmente me pregunté por qué lo siguiente se consideraba ambiguo:

template<class T> void f(T,T*); // 1 template<class T> void f(T, int*); // 2 f(0, (int*)0); // ambiguous

(The above is ambiguous because one cannot deduce f1(U1,U1*) from f2(T,int*), and going the other way, one cannot deduce f2(U2,int*) from f1(T,T*). Neither is more specialized.)

pero lo siguiente no sería ambiguo:

template<class T> struct X { typedef int type; }; template<class T> void f(T, typename X<T>::type*); // 3 template<class T> void f(T, int*); // 2

(La razón por la que uno podría esperar que sea ambigua es si sucediera lo siguiente:
- f3(U1,X<U1>::type*) -> f3(U1, int*) ==> f2(T,int*) (deduction ok, T=U1)
- f2(U2,int*) ==> f3(T, X<T>::type*) (deduction ok, T=U2 makes X<U2>::type* -> int*)
Si esto fuera cierto, ninguno sería más especializado que el otro.)

Después de estudiar el algoritmo de ordenamiento parcial de Clang, está claro que tratan el ''3'' anterior como si fuera:

template<class T, class S> void f(T, S*); // 4

así que la deducción de alguna ''U'' única contra ''nombre de tipo X :: tipo'' tendrá éxito -

  • f3(U1,X<U1>::type*) is treated as f3(U1, U2*) ==> f2(T,int*) (deduction not ok)
  • f2(U2,int*) ==> f3(T,S* [[X<T>::type*]]) (deduction ok, T=U2, S=int)

Y entonces ''2'' es claramente más especializado que ''3''.


Editar: ignore esta publicación. Después de estudiar el algoritmo de clangs para ordenamiento parcial implementado por Doug Gregor (aunque solo se implementó parcialmente en el momento de escribir esto, parece que la lógica relevante para la pregunta del OP se ha implementado de manera adecuada). aparece como si trata el contexto no deducido como solo otro parámetro de plantilla. Lo que sugiere que la sobrecarga con el argumento explícito void * debería ser la versión más especializada y no debería haber ambigüedad. Como de costumbre, Comeau está en lo cierto. Ahora, en cuanto a la redacción en el estándar que define claramente este comportamiento, ese es otro asunto ...

Dado que esta publicación también fue publicada en comp.lang.c ++. Moderada, y parece que también está causando confusión. Pensé que también publicaría mi respuesta a ese grupo, ya que la discusión obviamente es relevante para la pregunta que se hace aquí. .

On Jul 25, 1:11 pm, Bart van Ingen Schenau <[email protected]> wrote:

You are going one step too fast here. How do you know (and would the compiler know) that there is no specialisation of Const<Q> such that Const<Q>::type != void?

As far as I can see, the compiler would transform the parameter-list of A to: AT=(Q, <unknown>*). To call B with these parameters requires an implicit conversion (<unknown>* to void*) and therefore A is less specialised than B.

Creo que esto es incorrecto. Al verificar para ver qué función es más especializada (durante el pedido parcial), el compilador transforma la lista de parámetros en (Q, void*) , es decir, crea una instancia de la plantilla relevante (mejor coincidencia) y busca dentro de ella el valor de ''tipo'' - en este caso, basado en la plantilla primaria, será nulo *.

En cuanto a su punto relativo a la especialización parcial: cuando se comprueba qué plantilla es más especializada que la otra, el único tipo que se puede usar es el tipo único generado, si hay otras especializaciones en el momento de la creación de la declaración (cuando se resuelve la sobrecarga) hecho) serán considerados. Si los agrega más tarde, y deben seleccionarse, estará violando la ODR (según 14.7.4.1)

Las especializaciones parciales / explícitas también tendrán en cuenta durante la formación del conjunto candidato, pero esta vez utilizando los tipos de argumentos reales para la función. Si la mejor especialización parcial coincidente (de X) resulta en un tipo de función que tiene una mejor secuencia de conversión implícita para algún parámetro, entonces nunca llegamos a la fase de ordenamiento parcial, y esa función "mejor" será seleccionada (antes de realizar a la fase de ordenamiento parcial)

Aquí hay un ejemplo con comentarios sobre lo que debería estar sucediendo en varios pasos:

template<class T, bool=true> struct X; // Primary template<class T> struct X<T,true> { typedef T type; }; // A template<> struct X<int*,true> { typedef void* type; }; // B template<class T> void f(T,typename X<T>::type); //1 template<class T> void f(T*,void*); //2 int main() { void* pv; int* pi; f(pi,pi); // two candidate functions: f1<int*>(int*,void*), f2<int>(int*,void*) // Note: specialization ''B'' used to arrive at void* in f1 // neither has a better ICS than the other, so lets partially order // transformed f1 is f1<U1>(U1,X<U1,true>::type) --> f1<U1>(U1,U1) // (template ''A'' used to get the second U1) // obviously deduction will fail (U1,U1) -> (T*,void*) // and also fails the other way (U2*, void*) -> (T,X<T>::type) // can not partially order them - so ambiguity f(pv,pv); // two candidate functions: f1<void*>(void*,void*), f2<void>(void*,void*) // Note: specialization ''A'' used to arrive at second void* in f1 // neither has a better ICS than the other, so lets partially order // transformed f1 is f1<U1>(U1,X<U1>::type) --> f1<U1>(U1,U1) // (template ''A'' used to get the second U1) // obviously deduction will fail (U1,U1) -> (T*,void*) // and also fails the other way (U2*, void*) -> (T,X<T>::type) // can not partially order them - so ambiguity again }

También vale la pena mencionar que si la plantilla primaria no tiene una definición, entonces SFINAE opera durante la fase de ordenamiento parcial, ninguno de los dos puede deducirse y debe producirse ambigüedad.

Además, si agrega otra plantilla que daría lugar a otra coincidencia si el punto de instalación de cualquiera de esas funciones se mueve a otra parte en la unidad de traducción, violará claramente el ODR.

On Jul 25, 1:11 pm, Bart van Ingen Schenau <[email protected]> wrote:

En primer lugar, ser más especializado significa que hay menos tipos en los que esa plantilla se puede seleccionar mediante resolución de sobrecarga. Utilizando esto, las reglas para ordenamiento parcial se pueden resumir como: Trate de encontrar un tipo para A tal que A pueda llamarse pero B no, o que la resolución de sobrecarga prefiera llamar a A. Si ese tipo se puede encontrar, entonces B es más especializado que A.

No hay argumento aquí. Pero según las reglas tal como están actualmente, el ejemplo del OP debe ser ambiguo.

Finalmente, aquí hay respuestas explícitas e inequívocas a las dos preguntas específicas planteadas por litb:

1) ¿Esto ahora usará el valor de T deducido para el primer parámetro?
Sí, por supuesto, tiene que hacerlo, está haciendo la deducción del argumento de la plantilla, los ''enlaces'' deben mantenerse.

2) Ahora, ¿por qué las implementaciones dicen que el segundo es más especializado?
Porque están equivocados;)

Espero que esto solucione el problema. Por favor, avíseme si hay algo que todavía no está claro :)

Editar: litb planteó un buen punto en su comentario, tal vez afirmar que la plantilla primaria siempre se usará para la creación de instancias con el tipo generado único es una afirmación demasiado fuerte.
Hay instancias en las que no se llamará a la plantilla principal.
Lo que quiero decir es que cuando se produce un pedido parcial, se utiliza algún tipo único generado para que coincida con la mejor especialización. Tienes razón, no tiene que ser la plantilla principal. He editado el lenguaje anterior para hacerlo. También planteó un problema relacionado con la definición de una mejor plantilla de coincidencia después del momento de la instalación. Eso será una violación de la ODR según la sección sobre el punto de instanciación.

El estándar dice que una vez que los pares de A / P se crean (usando las reglas de transformación como se describe en temp.func.order) se deducen uno contra el otro usando la deducción de argumento de plantilla (temp.deduct) - y esa sección maneja el caso de contextos no deducidos, creando una instancia de la plantilla y su tipo anidado, desencadenando puntos de instancias. La sección de punto de temperatura maneja las violaciones de ODR (el significado de ordenamiento parcial no debe cambiar independientemente de los puntos de instalación dentro de una unidad de traducción). Todavía no estoy seguro de dónde viene la confusión? - Faisal Vali hace 1 hora [eliminar este comentario]

litb: "Tenga en cuenta que el paso que pone Q en Const :: tipo para construir los argumentos no está cubierto explícitamente por la regla SFINAE. Las reglas SFINAE funcionan con deducción de argumento, coloque los párrafos que ponen Q en la lista de parámetros de función de plantilla de función son en 14.5.5.2. ''

Las reglas de SFINAE deben usarse aquí, ¿cómo podrían no ser? Creo que está lo suficientemente implícito, no negaré que podría ser más claro, y aunque aliento al comité a aclarar esto, no creo que deba aclararse para interpretar su ejemplo lo suficiente.

Déjame proporcionar una forma de vincularlos. De (14.8.2): "Cuando se especifica una lista explícita de argumentos de plantilla, los argumentos de la plantilla deben ser compatibles con la lista de parámetros de la plantilla y deben dar como resultado un tipo de función válido como se describe a continuación; de lo contrario, la deducción del tipo falla".

Desde (14.5.5.2/3) "La transformación utilizada es: - Para cada parámetro de plantilla de tipo, sintetice un tipo único y reemplácelo por cada aparición de ese parámetro en la lista de parámetros de función, o para una función de conversión de plantilla, en la declaración tipo."

En mi opinión, la cita anterior implica que una vez que "crea" tipos generados únicos para cada parámetro de plantilla, la declaración de la función debe ser instanciada implícitamente suministrando explícitamente los tipos únicos como argumentos de plantilla a nuestra plantilla de función. Si esto da como resultado un tipo de función no válida, entonces no solo falla la transformación, pero más importante aún, la subsiguiente deducción del argumento de la plantilla para ordenar parcialmente la función.

Desde (14.5.5.2/4) "Utilizando la lista de parámetros de funciones transformadas, realice la deducción de argumentos con respecto a la otra plantilla de funciones. La plantilla transformada es al menos tan especializada como la otra si, y solo si , la deducción tiene éxito y los tipos de parámetros deducidos son una coincidencia exacta (por lo que la deducción no se basa en conversiones implícitas) ".

Si la lista de parámetros de la función transformada conduce a una falla de sustitución, entonces sabemos que la deducción no pudo haber tenido éxito. Y dado que la deducción no tuvo éxito, no es tan especializada como la otra, eso es todo lo que necesitamos saber para proceder en orden parcial de los dos.

litb: Tampoco estoy seguro de lo que sucede en este caso: template<typename T> struct A; template<typename T> void f(T, typename A<T>::type); template<typename T> void f(T*, typename A<T>::type); seguramente, eso es un código válido, pero al hacer un tipo ::, fallará porque en el contexto de definición de plantilla, A aún no está definido "También tenga en cuenta que no hay POI definido para instancias de plantilla resultantes de este tipo de sustitución al intentar determinar un orden (el orden parcial no depende de ningún contexto. Es una propiedad estática de dos plantillas de funciones involucradas). Creo que esto parece ser un problema en el Estándar que necesita ser reparado.

Ok, creo que veo dónde estamos viendo las cosas de manera diferente. Si te entiendo correctamente, estás diciendo que a medida que estas plantillas de funciones se declaran, el compilador mantiene una pista del orden parcial entre ellas, independientemente de la resolución de sobrecarga que alguna vez se active para seleccionar entre ellas. Si así es como lo interpreta, entonces puedo ver por qué esperaría el comportamiento anterior que describe. Pero no creo que el estándar alguna vez lo requiera o lo exija.

Ahora, el estándar es claro que el orden parcial es agnóstico al tipo que se usa para llamar a la función (creo que esto es a lo que se refiere cuando lo describe como una propiedad estática y es independiente del contexto).

El estándar también es claro que solo se preocupa por la ordenación parcial (invoca orden parcial) entre plantillas de funciones durante el proceso de resolución de sobrecarga (13.3.3 / 1) si y solo si no podía elegir la mejor función basada en ICS o si una es una plantilla y la otra no. [El ordenamiento parcial de las especializaciones parciales de la plantilla de clase es un tema aparte y en mi opinión usa el contexto relevante (otras definiciones de plantilla) que requiere la instanciación de esa clase en particular.]

Por lo tanto, en mi opinión, dado que la maquinaria de ordenamiento parcial de plantillas de funciones se invoca cuando se realiza una resolución de sobrecarga, debe usar una parte relevante del contexto (definiciones de plantilla y especializaciones) disponible en el punto en que se realiza la resolución de sobrecarga .

Entonces, basado en mi interepretación, de acuerdo con su ejemplo usando ''template struct A'' arriba, el código es válido. El orden parcial no se realiza en el contexto de definición. Pero si / cuando invocas una resolución de sobrecarga entre las dos funciones escribiendo una llamada a f ((int *) 0,0) - y en ese momento cuando el compilador intenta armar una declaración candidata o pedirla parcialmente (si llega al paso de pedido parcial) si una expresión o tipo inválido resulta como parte del tipo de función, SFINAE nos ayuda y nos dice que la deducción de la plantilla falla (en lo que respecta al orden parcial, eso implica que uno no puede ser más especializado que el otro si ni siquiera pudimos transformar la plantilla).

Ahora, en lo que respecta a los PDI, si está convencido de que los tipos de funciones transformadas representan representaciones implícitas utilizando listas de argumentos de plantilla suministrados explícitamente (utilizando los tipos generados de manera única), las siguientes citas estándar son relevantes:

14.6.4.1/1 Para una especialización de plantilla de función, una especialización de plantilla de función miembro o una especialización para una función miembro o miembro de datos estáticos de una plantilla de clase, si la especialización se instancia de forma implícita porque se hace referencia desde otra especialización de plantilla y la el contexto del que se hace referencia depende de un parámetro de plantilla, el punto de ejemplificación de la especialización es el punto de instanciación de la especialización circundante.

La forma en que interpreto esto es que el POI del tipo de función transformada y el tipo de función origianl es el mismo que el POI para las funciones creadas por la llamada a la función real.

litb: dado que el ordenamiento parcial es solo a property of the syntactic form of parameters (ie "T*" against "T(*)[N]"), votaría por modificar la especificación (como "si Q aparece en un anidado nombre especificador de un ID calificado que nombra un tipo, luego el tipo nombrado es "Q") O diciendo que el tipo nombrado es otro tipo único. This means that in template<typename T> void f(T, typename Const<T>::type*); the argument list is (Q, R*), for example. Same for template<typename T> void f(T*, typename ConstI<sizeof(T)>::type); the arg lisst would be (Q*, R). A similar rule would be needed for non-type parameters, of course. Tendría que pensar en ello y hacer algunos casos de prueba para ver si esto daría ordenes naturales, sin embargo.

Aah: ahora estás sugiriendo una posible solución que resuelva la ambigüedad a favor de lo que todos esperamos intuitivamente, este es un problema aparte, y aunque me gusta la dirección en la que te diriges, como tú, yo también tendría que pensar un poco en ella antes de proclamar su viabilidad.

Gracias por continuar la discusión. Desearía que SO no solo te limitara a hacer comentarios.

Como puedes editar mis publicaciones, no dudes en responder dentro de la publicación si es más fácil.