and c++ templates language-lawyer c++17 constexpr

c++ - Implementando is_constexpr_copiable



constexpr in c++ (1)

Intenté implementar una plantilla de valor similar a std::is_constructible con la excepción de que solo sea verdadera cuando el tipo es copiable en un entorno constexpr (es decir, su constructor de copia es constexpr calificado). Llegué al siguiente código:

#include <type_traits> struct Foo { constexpr Foo() = default; constexpr Foo(const Foo&) = default; }; struct Bar { constexpr Bar() = default; Bar(const Bar&); }; namespace detail { template <int> using Sink = std::true_type; template<typename T> constexpr auto constexpr_copiable(int) -> Sink<(T(T()),0)>; template<typename T> constexpr auto constexpr_copiable(...) -> std::false_type; } template<typename T> struct is_constexpr_copiable : decltype(detail::constexpr_copiable<T>(0)){ }; static_assert( is_constexpr_copiable<Foo>::value, ""); static_assert(!is_constexpr_copiable<Bar>::value, "");

Ahora me pregunto si esto está de acuerdo con el estándar, ya que los compiladores parecen estar en desacuerdo sobre la salida. https://godbolt.org/g/Aaqoah

Editar (c ++ 17 características):

Al implementar is_constexpr_constructible_from algo diferente, con el nuevo tipo de plantilla auto no is_constexpr_constructible_from c ++ 17, una vez más encontré una diferencia entre compiladores, al eliminar la referencia de nullptr en una expresión SFINAE con SFINAE .

#include <type_traits> struct Foo { constexpr Foo() = default; constexpr Foo(const Foo&) = default; constexpr Foo(const Foo*f):Foo(*f) {}; }; struct Bar { constexpr Bar() = default; Bar(const Bar&); }; namespace detail { template <int> struct Sink { using type = std::true_type; }; template<typename T, auto... t> constexpr auto constexpr_constructible_from(int) -> typename Sink<(T(t...),0)>::type; template<typename T, auto... t> constexpr auto constexpr_constructible_from(...) -> std::false_type; } template<typename T, auto... t> struct is_constexpr_constructible_from : decltype(detail::constexpr_constructible_from<T, t...>(0)){ }; constexpr Foo foo; constexpr Bar bar; static_assert( is_constexpr_constructible_from<Foo, &foo>::value, ""); static_assert(!is_constexpr_constructible_from<Foo, nullptr>::value, ""); static_assert(!is_constexpr_constructible_from<Bar, &bar>::value, ""); int main() {}

https://godbolt.org/g/830SCU

Edición: (abril 2018)

Ahora que ambos compiladores supuestamente tienen soporte para C ++ 17, he encontrado que el siguiente código funciona aún mejor (no requiere un constructor predeterminado en `T`), pero solo en el clang. Todo sigue igual, pero reemplace el `nombre` del espacio de nombres con lo siguiente: detalle del espacio de nombres {template struct Sink {}; plantilla constexpr auto sink (S) -> std :: true_type; plantilla constexpr auto try_copy () -> Sink; plantilla constexpr auto constexpr_copiable (int) -> decltype (sink (std :: declval, 0) >> ())); plantilla constexpr auto constexpr_copiable (...) -> std :: false_type; } https://godbolt.org/g/3fB8jt Esto profundiza en partes del estándar sobre el contexto no evaluado, y ambos compiladores se niegan a permitir reemplazar `const T *` con `const T &` y usar `std :: declval ( ) `en lugar de` nullptr`-cast. Si obtengo la confirmación de que el comportamiento de Clang es el comportamiento estándar aceptado, elevaré esta versión a una respuesta, ya que solo requiere exactamente lo que se ha pedido.

Clang acepta algún comportamiento indefinido, nullptr referencia nullptr , en la evaluación de un operando no decltype de decltype .


El más difícil de los desafíos es dar una sola función para evaluar si un constructor constexpr de const T& existe para T arbitraria, dado aquí parece difícilmente posible en C ++ 17. Por suerte, podemos recorrer un largo camino sin llegar. El razonamiento para esto es el siguiente:

Conociendo el espacio problemático.

Las siguientes restricciones son importantes para determinar si una expresión se puede evaluar en contenido constexpr :

  • Para evaluar el constructor de copia de T , se necesita un valor de tipo const T& . Dicho valor debe referirse a un objeto con vida útil activa, es decir, en el contexto constexpr debe referirse a algún valor creado en una expresión que encierra lógicamente.

  • Para crear esta referencia como resultado de la promoción temporal para T arbitraria, tendríamos que conocer y llamar a un constructor, cuyos argumentos podrían involucrar otras expresiones virtualmente arbitrarias cuya constexpr sería necesario evaluar. Parece que esto requiere resolver el problema general de determinar la constexpr de las expresiones generales, hasta donde puedo entender. ¹

  • ¹ En realidad, si cualquier constructor con argumentos, incluido el constructor de copia, se define como constexpr , debe haber alguna forma válida de construir una T , ya sea como inicialización agregada o mediante un constructor. De lo contrario, el programa estaría mal formado, como pueden determinar los requisitos del especificador constexpr §10.1.5.5 :

    Para una función constexpr o constructor constexpr que no tiene un valor predeterminado ni una plantilla, si no existen valores de argumento tales que una invocación de la función o el constructor pueda ser una subexpresión evaluada de una expresión de la constante central, o, para un constructor, un inicializador constante para En algún objeto ([basic.start.static]), el programa está mal formado, no se requiere diagnóstico.

    Esto podría darnos una pequeña laguna.

  • Por lo tanto, la mejor expresión es un operando no evaluado §8.2.3.1

    En algunos contextos, aparecen operandos no evaluados ([expr.prim.req], [expr.typeid], [expr.sizeof], [expr.unary.noexcept], [dcl.type.simple], [temp]). Un operando no evaluado no es evaluado

  • Los operandos no evaluados son expresiones generales, pero no se les puede exigir que sean evaluables en el momento de la compilación, ya que no se evalúan en absoluto. Tenga en cuenta que los parámetros de una plantilla no forman parte de la expresión no evaluada en sí, sino que forman parte de la identificación no calificada que da nombre al tipo de plantilla. Eso fue parte de mi confusión original y trata de encontrar una posible implementación.

  • Los argumentos de plantilla que no son de tipo deben ser expresiones constantes §8.6, pero esta propiedad se define a través de la evaluación (que ya hemos determinado que no es posible en general). §8.6.2

    Una expresión e es una expresión constante central a menos que la evaluación de e, siguiendo las reglas de la máquina abstracta, evalúe [resaltado por mí mismo] una de las siguientes expresiones:

  • El uso de noexpect para el contexto no evaluado tiene el mismo problema: el mejor discriminador, el noexceptness inferido, funciona solo en llamadas de función que pueden evaluarse como una expresión de núcleo-constante, por lo que el truco mencionado en esta respuesta de no funciona.

  • sizeof tiene los mismos problemas que decltype . Las cosas pueden cambiar con los concepts .

  • La nueva introducción if constexpr es, lamentablemente, una expresión, sino una declaración con un argumento de expresión. Por lo tanto, no puede ayudar a imponer la capacidad de constexpr constexpr de una expresión. Cuando se evalúa la afirmación, también lo es su expresión y estamos de vuelta en el problema de crear una const T& evaluable const T& . Las declaraciones descartadas no tienen influencia en el proceso en absoluto.

Posibilidades fáciles primero

Dado que la parte difícil es crear const T& , simplemente lo hacemos para un pequeño número de posibilidades comunes pero fáciles de determinar, y dejamos el resto a la especialización por parte de personas extremadamente especiales.

namespace detail { template <int> using Sink = std::true_type; template<typename T,bool SFINAE=true> struct ConstexprDefault; template<typename T> struct ConstexprDefault<T, Sink<(T{}, 0)>::value> { inline static constexpr T instance = {}; }; template<typename T> constexpr auto constexpr_copiable(int) -> Sink<(T{ConstexprDefault<T>::instance}, 0)>; template<typename T> constexpr auto constexpr_copiable(...) -> std::false_type; } template<typename T> using is_constexpr_copyable_t = decltype(detail::constexpr_copiable<T>(0));

details::ConstexprDefault especialización details::ConstexprDefault debe ser posible para cualquier tipo de clase que declare un constructor de copia constexpr, como se vio anteriormente. Tenga en cuenta que el argumento no se mantiene para otros tipos compuestos que no tienen constructores §6.7.2 . Arreglos, uniones, referencias y enumeraciones necesitan consideraciones especiales.

Un ''conjunto de pruebas'' con una multitud de tipos se puede encontrar en godbolt . Un gran agradecimiento para reddit user / u / dodheim de quien lo copié . Las especializaciones adicionales para los tipos de compuestos faltantes se dejan como un ejercicio para el lector.

² o What does this leave us with?

El error de evaluación en los argumentos de la plantilla no es fatal. SFINAE hace posible cubrir una amplia gama de posibles constructores. El resto de esta sección es puramente teórico, no es agradable para los compiladores y, de lo contrario, podría ser simplemente estúpido.

Es potencialmente posible enumerar muchos constructores de un tipo usando métodos similares a magic_get . Esencialmente, use un tipo Ubiq pretenda ser convertible a todos los demás tipos para falsificar su camino a través de decltype(T{ ubiq<I>()... }) donde I es un paquete de parámetros con el recuento de inicializador inspeccionado y la template<size_t i> Ubiq ubiq() simplemente genera la cantidad correcta de instancias. Por supuesto, en este caso, la conversión a T tendría que ser explícitamente rechazada.

¿Por qué sólo muchos? Como antes, existirá algún constructor constexpr pero podría tener restricciones de acceso. Esto daría un falso positivo en nuestra máquina de plantillas y conduciría a una búsqueda infinita, y en algún momento el compilador morirá: /. O el constructor puede estar oculto por una sobrecarga que no se puede resolver ya que Ubiq es demasiado general. Mismo efecto, triste compilador y un furioso PETC (Personas para el tratamiento ético de los compiladores ™, no una organización real). En realidad, las restricciones de acceso pueden resolverse por el hecho de que no se aplican en los argumentos de la plantilla, lo que nos permite extraer un puntero a miembro y [...].

Me detendré aquí Por lo que puedo decir, es tedioso y en su mayoría innecesario. Seguramente, cubrir posibles invocaciones de constructores hasta 5 argumentos será suficiente para la mayoría de los casos de uso. La T arbitraria es muy, muy difícil y es mejor que esperemos a C ++ 20 ya que la metaprogramación de la plantilla está nuevamente a punto de cambiar de forma masiva.