c++ - guia - versiones de qgis



¿Por qué importa el orden de la sustitución del argumento de la plantilla? (1)

Como se afirma, C ++ 14 dice explícitamente que el orden de sustitución de los argumentos de la plantilla está bien definido; más específicamente, se garantizará que proceda en "orden léxico" y se detendrá siempre que una sustitución provoque la deducción.

Comparado con C ++ 11 será mucho más fácil escribir el código SFINAE que consiste en una regla que depende de otra en C ++ 14, también nos alejaremos de los casos en que el orden indefinido de la sustitución de la plantilla puede hacer que toda nuestra aplicación sufra comportamiento indefinido.

Nota : Es importante tener en cuenta que el comportamiento descrito en C ++ 14 siempre ha sido el comportamiento previsto, incluso en C ++ 11, simplemente que no se ha redactado de una manera tan explícita.

¿Cuál es la razón de este cambio?

La razón original detrás de este cambio se puede encontrar en un informe de defectos presentado originalmente por Daniel Krügler :

EXPLICACIÓN ADICIONAL

Al escribir SFINAE, nosotros como desarrolladores dependemos del compilador para encontrar cualquier sustitución que arroje un tipo o expresión no válida en nuestra plantilla cuando se utilice. Si se encuentra dicha entidad no válida, nos gustaría ignorar lo que declare la plantilla y continuar para encontrar una coincidencia adecuada.

La falla de sustitución no es un error , sino un mero ... "aw, esto no funcionó ... por favor, adelante" .

El problema es que los posibles tipos y expresiones inválidos solo se buscan en el contexto inmediato de la sustitución.

14.8.2 - Deducción del argumento de la plantilla - [temp.deduct]

8 Si una sustitución da como resultado un tipo o expresión no válida, escriba deducción falla. Un tipo o expresión inválida es aquella que estaría mal formada si se escribiera utilizando los argumentos sustituidos.

[ Nota: la verificación de acceso se realiza como parte del proceso de sustitución. - nota final ]

Solo los tipos y expresiones no válidos en el contexto inmediato del tipo de función y sus tipos de parámetros de plantilla pueden provocar un error de deducción.

[ Nota: la evaluación de los tipos y expresiones sustituidos puede dar como resultado efectos secundarios tales como la instanciación de especializaciones de plantilla de clase y / o especializaciones de plantilla de función, la generación de funciones implícitamente definidas, etc. Tales efectos secundarios no están en el "inmediato contexto "y puede dar lugar a que el programa esté mal formado. - nota final ]

En otras palabras, una sustitución que se produce en un contexto no inmediato aún hará que el programa se forme mal, por lo que el orden de las sustituciones de plantilla es importante; puede cambiar todo el significado de una determinada plantilla.

Más específicamente, puede ser la diferencia entre tener una plantilla que se puede usar en SFINAE y una plantilla que no lo es .

EJEMPLO SILLY

template<typename SomeType>
struct inner_type { typedef typename SomeType::type type; };

template<
  class T,
  class   = typename T::type,            // (E)
  class U = typename inner_type<T>::type // (F)
> void foo (int);                        // preferred

template<class> void foo (...);          // fallback

struct A {                 };  
struct B { using type = A; };

int main () {
  foo<A> (0); // (G), should call "fallback "
  foo<B> (0); // (H), should call "preferred"
}

En la línea marcada (G) queremos que el compilador compruebe primero (E) y si eso sucede evalúa (F) , pero antes del cambio estándar discutido en esta publicación no había tal garantía.


El contexto inmediato de las sustituciones en foo(int) incluye;

  • (E) asegurándose de que el aprobado en T tiene ::type
  • (F) asegurándose de que inner_type<T> tiene ::type


Si (F) se evalúa a pesar de que (E) da como resultado una sustitución inválida, o si (F) se evalúa antes (E) nuestro ejemplo corto (tonto) no hará uso de SFINAE y obtendremos un diagnóstico que diga que nuestro la aplicación está mal formada ... a pesar de que teníamos la intención de usar foo(...) en tal caso.


Nota: Tenga en cuenta que SomeType::type no está en el contexto inmediato de la plantilla; una falla en typedef dentro de inner_type hará que la aplicación se forme mal y evitará que la plantilla haga uso de SFINAE .

¿Qué implicaciones tendrá esto en el desarrollo del código en C ++ 14?

El cambio facilitará drásticamente la vida de los abogados de idiomas que intentan implementar algo que se garantiza que se evaluará de cierta manera (y orden), sin importar qué compilador conforme los estén usando.

También hará que la sustitución de los argumentos de la plantilla se comporte de una manera más natural para los que no son juristas ; hacer que la sustitución ocurra de izquierda a derecha es mucho más intuitiva que erhm-like-any-way-the-compiler-wanna-do-it-like-erhm -....

¿No hay ninguna implicación negativa?

Lo único que se me ocurre es que dado que el orden de sustitución ocurrirá de izquierda a derecha, un compilador no puede manejar múltiples sustituciones a la vez usando una implementación asincrónica.

Todavía tengo que tropezar con esa implementación, y dudo que resulte en un aumento importante del rendimiento, pero al menos el pensamiento (en teoría) se ajusta un poco al lado "negativo" de las cosas.

Como ejemplo: un compilador no podrá usar dos subprocesos que hagan sustituciones al instante cuando instalen una plantilla determinada sin ningún mecanismo que actúe como las sustituciones que ocurrieron después de que nunca sucediera un cierto punto, si es necesario.

La historia

Nota : Un ejemplo que podría haberse tomado de la vida real se presentará en esta sección para describir cuándo y por qué importa el orden de la sustitución del argumento de la plantilla. Por favor, hágamelo saber (usando la sección de comentarios) si algo no es lo suficientemente claro, o tal vez incluso incorrecto.

Imagine que estamos trabajando con enumeradores y que nos gustaría obtener fácilmente el valor subyacente de la enumeración especificada.

Básicamente, estamos hartos y cansados ​​de tener que escribir siempre (A) , cuando lo ideal es que queramos algo más cerca de (B) .

auto value = static_cast<std::underlying_type<EnumType>::type> (SOME_ENUM_VALUE); // (A)

auto value = underlying_value (SOME_ENUM_VALUE);                                  // (B)

LA IMPLEMENTACIÓN ORIGINAL

Dicho y hecho, decidimos escribir una implementación de underlying_value con el siguiente aspecto.

template<class T, class U = typename std::underlying_type<T>::type> 
U underlying_value (T enum_value) { return static_cast<U> (enum_value); }

Esto aliviará nuestro dolor y parece hacer exactamente lo que queremos; pasamos un enumerador y recuperamos el valor subyacente.

Nos decimos a nosotros mismos que esta implementación es increíble y le pedimos a un colega nuestro ( Don Quijote ) que se siente y revise nuestra implementación antes de llevarla a producción.

LA REVISIÓN DEL CÓDIGO

Don Quijote es un experimentado desarrollador de C ++ que tiene una taza de café en una mano y el estándar de C ++ en la otra. Es un misterio cómo logra escribir una sola línea de código con ambas manos ocupadas, pero esa es una historia diferente.

Él revisa nuestro código y llega a la conclusión de que la implementación no es segura, necesitamos proteger std::underlying_type del comportamiento indefinido ya que podemos pasar un T que no es del tipo de enumeración .

20.10.7.6 - Otras transformaciones - [meta.trans.other]

template<class T> struct underlying_type;

Condición: T será un tipo de enumeración (7.2)
Comentarios: El type typedef miembro nombrará el tipo subyacente de T

Nota: El estándar especifica una condición para el tipo underlying_type , pero no va más allá para especificar qué sucederá si se crea una instancia con un no enum . Dado que no sabemos qué sucederá en tal caso, el uso cae dentro de un comportamiento indefinido ; podría ser puro UB , hacer que la aplicación esté mal formada u ordenar ropa interior comestible en línea.

EL CABALLERO EN ARMADURA BRILLANTE

Don grita algo acerca de cómo siempre debemos respetar el estándar de C ++, y que debemos sentir una tremenda vergüenza por lo que hemos hecho ... es inaceptable.

Después de que se calmó y tomó unos sorbos más de café, sugiere que modifiquemos la implementación para agregar protección contra la std::underlying_type instancias de std::underlying_type con algo que no está permitido.

template<
  typename T,
  typename   = typename std::enable_if<std::is_enum<T>::value>::type,  // (C)
  typename U = typename std::underlying_type<T>::type                  // (D)
>
U underlying_value (T value) { return static_cast<U> (value); }

EL MOLINO DE VIENTO

Agradecemos a Don por sus descubrimientos y ahora estamos satisfechos con nuestra implementación, pero solo hasta que nos damos cuenta de que el orden de sustitución del argumento de la plantilla no está bien definido en C ++ 11 (ni se indica cuándo se detendrá la sustitución).

Compilado como C ++ 11, nuestra implementación todavía puede causar una instanciación de std::underlying_type con una T que no sea del tipo de enumeración debido a dos razones:

  1. El compilador puede evaluar (D) antes de (C) ya que la orden de sustitución no está bien definida, y;

  2. incluso si el compilador evalúa (C) antes (D) , no está garantizado que no evaluará (D) , C ++ 11 no tiene una cláusula que diga explícitamente cuándo se debe detener la cadena de sustitución.

La implementación de Don estará libre de comportamiento indefinido en C ++ 14, pero solo porque C ++ 14 establece explícitamente que la sustitución procederá en orden léxico , y que se detendrá siempre que una sustitución haga que la deducción falle .

Puede que Don no esté luchando contra los molinos de viento en este caso, pero seguramente se perdió un dragón muy importante en el estándar C ++ 11.

Una implementación válida en C ++ 11 tendría que asegurarse de que, independientemente del orden en que se produzca la sustitución de los parámetros de la plantilla, la instancia de std::underlying_type no tenga un tipo no válido.

#include <type_traits>

namespace impl {
  template<bool B, typename T>
  struct underlying_type { };

  template<typename T>
  struct underlying_type<true, T>
    : std::underlying_type<T>
  { };
}

template<typename T>
struct underlying_type_if_enum
  : impl::underlying_type<std::is_enum<T>::value, T>
{ };

template<typename T, typename U = typename underlying_type_if_enum<T>::type>
U get_underlying_value (T value) {
  return static_cast<U> (value);  
}

Nota: se usó tipo_incrustado porque es una manera simple de usar algo en el estándar contra lo que está en el estándar; lo importante es que crear una instancia con un non-enum es un comportamiento indefinido .

El informe de defectos vinculado anteriormente en esta publicación utiliza un ejemplo mucho más complejo que supone un amplio conocimiento sobre el asunto. Espero que esta historia sea una explicación más adecuada para aquellos que no están bien informados sobre el tema.

C ++ 11

14.8.2 - Deducción del argumento de la plantilla - [temp.deduct]

7 La sustitución ocurre en todos los tipos y expresiones que se usan en el tipo de función y en las declaraciones de parámetros de plantilla. Las expresiones incluyen no solo expresiones constantes como las que aparecen en los límites de la matriz o como argumentos de plantilla sin tipo, sino también expresiones generales (es decir, expresiones no constantes) dentro de sizeof , decltype y otros contextos que permiten expresiones no constantes.

C ++ 14

14.8.2 - Deducción del argumento de la plantilla - [temp.deduct]

7 La sustitución ocurre en todos los tipos y expresiones que se usan en el tipo de función y en las declaraciones de parámetros de plantilla. Las expresiones incluyen no solo expresiones constantes como las que aparecen en los límites de la matriz o como argumentos de plantilla sin tipo, sino también expresiones generales (es decir, expresiones no constantes) dentro de sizeof , decltype y otros contextos que permiten expresiones no constantes. La sustitución procede en orden léxico y se detiene cuando se encuentra una condición que hace que la deducción falle .

La oración agregada establece explícitamente el orden de sustitución cuando se trata de parámetros de plantilla en C ++ 14.

El orden de sustitución es algo que a menudo no recibe mucha atención. Todavía tengo que encontrar un solo artículo sobre por qué esto es importante. Tal vez esto se deba a que C ++ 1y aún no se ha estandarizado por completo, pero supongo que ese cambio se debe haber introducido por algún motivo.

La pregunta:

  • ¿Por qué y cuándo importa el orden de la sustitución del argumento de la plantilla?




c++14