Cómo resolver el problema de "la lectura de la variable 'a' no constexpr no está permitida en una expresión constante" con boost.hana


8

Estoy usando c ++ 17 con Boost.hana para escribir algunos programas de metaprogramación. Un problema que me atrapó es qué tipo de expresión se puede usar en un contexto constexpr como static_assert. Aquí hay un ejemplo:

#include <boost/hana.hpp>

using namespace boost::hana::literals;

template <typename T>
class X {
public:
    T data;

    constexpr explicit X(T x) : data(x) {}

    constexpr T getData() {
        return data;
    }
};


int main() {
    {   // test1
        auto x1 = X(1_c);
        static_assert(x1.data == 1_c);
        static_assert(x1.getData() == 1_c);
    }
    {   //test2.1
        auto x2 = X(boost::hana::make_tuple(1_c, 2_c));
        static_assert(x2.data[0_c] == 1_c);

        // static_assert(x2.getData()[0_c] == 1_c); // read of non-constexpr variable 'x2' is not allowed in a constant expression
    }
    {   //test2.2
        auto x2 = X(boost::hana::make_tuple(1_c, 2_c));
        auto data = x2.getData();
        static_assert(data[0_c] == 1_c);
    }
}

Primero escribo una clase X con datos de campo y un descriptor de acceso getData () . En el main () 's test1 parte, x1.data y x1.getData () se comportan igual que yo esperaba. Pero en la parte test2 , cambiar el argumento a un boost :: la tupla de hana, static_assert(x2.data[0_c] == 1_c)todavía se comporta bien pero static_assert(x2.getData()[0_c] == 1_c)falla la compilación, con el error de ' leer la variable no constexpr' x2 'no está permitido en una expresión constante '. Lo que es weired si divido x2.getData()[0_c]en auto data = x2.getData();y static_assert(data[0_c] == 1_c);compila bien de nuevo. Esperaría que se comporten igual. Entonces, ¿alguien puede ayudar a explicar por qué x2.getData()[0_c]no se puede usar en static_assert en este ejemplo?

Para reproducir: clang ++ 8.0 -I / ruta / a / hana-1.5.0 / include -std = c ++ 17 Test.cpp


Curiosamente, GCC lo compila con éxito . He reducido el código a este más corto .
xskxzr

constexprfalta en x2y data, consten getData. godbolt.org/z/ZNL2BK
Maxim Egorushkin

@xskxzr Lamentablemente, solo tengo que usar Clang para mi objetivo.
Largo

Respuestas:


5

El problema es que boost::hana::tupleno tiene un constructor de copia.

Tiene un constructor que se parece a un constructor de copia:

template <typename ...dummy, typename = typename std::enable_if<
    detail::fast_and<BOOST_HANA_TT_IS_CONSTRUCTIBLE(Xn, Xn const&, dummy...)...>::value
>::type>
constexpr tuple(tuple const& other)
    : tuple(detail::from_index_sequence_t{},
            std::make_index_sequence<sizeof...(Xn)>{},
            other.storage_)
{ }

Pero como se trata de una plantilla, no es un constructor de copias .

Como boost::hana::tupleno tiene un constructor de copia, uno se declara implícitamente y se define como predeterminado (no se suprime ya boost::hana::tupleque no tiene ningún constructor de copia ni movimiento ni operadores de asignación, porque, lo adivinó, no pueden ser plantillas).

Aquí vemos divergencia de implementación , demostrada en el comportamiento del siguiente programa:

struct A {
    struct B {} b;
    constexpr A() {};
    // constexpr A(A const& a) : b{a.b} {}    // #1
};
int main() {
    auto a = A{};
    constexpr int i = (A{a}, 0);
}

gcc acepta, mientras que Clang y MSVC rechazan, pero aceptan si la línea no #1está comentada. Es decir, los compiladores no están de acuerdo sobre si el constructor de copias definido implícitamente de una clase no (directamente) vacía se puede usar dentro del contexto de evaluación constante.

Según la definición del constructor de copia implícitamente definido, no hay forma de que el n. ° 1 sea diferente de constexpr A(A const&) = default;modo que gcc sea correcto. Tenga en cuenta también que si le damos a B un constructor de copia constexpr definido por el usuario, Clang y MSVC nuevamente lo aceptan, por lo que el problema parece ser que estos compiladores no pueden rastrear la constructibilidad de la copia constexpr de clases recursivamente vacías implícitamente copiables. Errores archivados para MSVC y Clang ( corregidos para Clang 11).

Tenga en cuenta que el uso de operator[]es un arenque rojo; El problema es si los compiladores permiten la llamada a getData()(que construye copias T) dentro de un contexto de evaluación constante como static_assert.

Obviamente, la solución ideal sería que Boost.Hana corrija de boost::hana::tuplemanera que tenga constructores de copia / movimiento reales y operadores de asignación de copia / movimiento. (Esto solucionaría su caso de uso ya que el código llamaría a constructores de copias proporcionados por el usuario, que están permitidos en un contexto de evaluación constante). Como solución alternativa , podría considerar piratear getData()para detectar el caso de no estado T:

constexpr T getData() {
    if (data == T{})
        return T{};
    else
        return data;
}

No entiendo por qué el constructor de copias predeterminado no funciona ... ¿Puedes intentar explicármelo?
Antoine Morrier

@AntoineMorrier Creo que esto es un error en clang; parece tener dificultades para reconocer que las clases recursivamente vacías se pueden copiar en el contexto constexpr.
ecatmur

Gracias. Muy útil respuesta! Entonces, ¿puedes explicar por qué la test2.2parte aceptada por Clang? (Edité la pregunta original y dividí test2 en test2.1 y test2.2) Esperaba que se comportaran igual.
Largo

@Largo que Clang no está contento es la construcción de copia hana::tupleque se produce en el retorno de getData. En test2.2 la copia se produce fuera del contexto de evaluación constante, por lo que Clang está bien con ella.
ecatmur

er ... es un poco difícil, al menos para mí entenderlo ... getData()no está permitido aquí, pero salir y presentar un temperal y luego aceptado ...
Long

1

El problema se debe a que está intentando recuperar un valor de tiempo de ejecución y probarlo en la compilación.

Lo que puede hacer es forzar la expresión en tiempo de compilación a través de decltypeay funcionará como un encanto :).

static_assert(decltype(x2.getData()[0_c]){} == 1_c);

#include <boost/hana.hpp>

using namespace boost::hana::literals;

template <typename T>
class X {
public:
    T data;

   constexpr explicit X(T x) : data(x) {}

   constexpr T getData() {
        return data;
    }
};


int main() {
    {   // test1
        auto x1 = X(1_c);
        static_assert(x1.data == 1_c);
        static_assert(x1.getData() == 1_c);
    }
    {   //test2
        auto x2 = X(boost::hana::make_tuple(1_c, 2_c));
        static_assert(x2.data[0_c] == 1_c);

         static_assert(decltype(x2.getData()[0_c]){} == 1_c);

        auto data = x2.getData();
        static_assert(data[0_c] == 1_c);
    }
}

Ahora la expresión se evalúa en tiempo de compilación, por lo que el tipo se conoce en tiempo de compilación y, dado que también es construible en tiempo de cálculo, es posible usarlo dentro de un static_assert


Gracias por la respuesta. Puedo entender que usar static_assert(decltype(x2.getData()[0_c]){} == 1_c)puede funcionar bien, pero todavía quiero guardarlo decltypeporque eso ahorraría mucho. Supongo que está diciendo que x2.getData()está recuperando un valor de tiempo de ejecución, por lo que no debería aparecer en una expresión static_assert. Entonces no entiendo por qué la x1.getData()parte de prueba1 y los datos de la data[0_c]parte de prueba2 pueden funcionar bien. ¿Cuáles son sus diferencias?
Largo

Realmente no se accede a ningún valor de tiempo de ejecución en ningún caso, pero tal vez el estándar no permite que el compilador lo verifique.
Jason Rice

0

Entonces, antes que nada, te falta el calificador const en el getData()método, por lo que debería ser:

constexpr T getData() const

No se promueve ninguna variable, al menos desde el punto de vista estándar, para ser constexpr si no está marcada como constexpr.

Tenga en cuenta que esto no es necesario para lo x1que es de tipo Xespecializado con hana :: integral_constant, ya que el resultado de 1_ces un tipo sin constructor de copia definido por el usuario, que no contiene ningún dato internamente, por lo que una operación de copia getData()es de hecho un no-op , por lo tanto, la expresión: static_assert(x1.getData() == 1_c); está bien, ya que no se ha realizado una copia real (ni el acceso al thispuntero no constante x1es necesario).

Esto es muy diferente para su contenedor con el hana::tuplecual contiene una construcción de copia de hana::tupledatos desde el x2.datacampo. Esto requiere acceso factual a su thispuntero, que no era necesario en el caso de x1que tampoco era una variable constexpr.

Esto significa que está expresando su intención equivocada con ambos x1y x2, al menos x2, es necesario marcar estas variables como constexpr. Tenga en cuenta también que el uso de tuplas vacías, que es una especialización general básicamente vacía (sin constructores de copia definidos por el usuario) hana::tuple, funciona a la perfección (sección test3):

#include <boost/hana.hpp>

using namespace boost::hana::literals;

template <typename T>
class X {
public:
    T data;

    constexpr explicit X(T x) : data(x) {}

    constexpr T getData() const {
        return data;
    }
};

template<typename V>
constexpr auto make_X(V value)
{
    return value;
}

int main() {
    {   // test1
        auto x1 = X(1_c);
        static_assert(x1.data == 1_c);
        static_assert(x1.getData() == 1_c);
    }
    {   //test2
        constexpr auto x2 = X(boost::hana::make_tuple(1_c, 2_c));
        static_assert(x2.data[0_c] == 1_c);

        static_assert(x2.getData()[0_c] == 1_c); // read of non-constexpr variable 'x2' is not allowed in a constant expression

        auto data = x2.getData();
        static_assert(data[0_c] == 1_c);
    }
    {   //test3
        auto x3 = X(boost::hana::make_tuple());
        static_assert(x3.data == boost::hana::make_tuple());

        static_assert(x3.getData() == boost::hana::make_tuple());
    }
}

Todavía no leí toda la respuesta, pero un método constexpr puede ser no constante normalmente.
Antoine Morrier

No estoy de acuerdo con que x1sea ​​un tipo vacío. Cualquier instancia de Xtiene un miembro de datos. También hana::tuplecontiene tipos vacíos en sí mismo, ya que utiliza una optimización de base vacía. Es posible que esté en el destino culpando al constructor de la copia porque Clang o libc ++ podrían estar haciendo algo raro std::integral_constant.
Jason Rice

Y, ¿hay alguna manera de que no tenga que agregar un constexpr para la declaración x2? Me gustaría que X se pueda inicializar con valor constante y valor de tiempo de ejecución. Tales como: `` `int g = 1; int main () {{/ * test3 * / auto x3 = X (g); }} `` `` Espero que también pueda funcionar perfectamente. Pero agregar un constexpr a x3 no se compilará, con un error:constexpr variable 'x3' must be initialized by a constant expression
Largo

@AntoineMorrier: Sí, pero esto está bien siempre que no esté utilizando el const thispuntero y desafortunadamente lo esté utilizando x2en el caso static_assert. (en el caso de x1, es una discusión adicional:)).
Michał Łoś

@JasonRice: Sí, es cierto, modificaré la respuesta, ya que no era precisa: ambos, por supuesto, no tienen ningún campo no estático. Aún así, y esto es lo que le falta a mi respuesta, tenga en cuenta que si bien hana::integral_constanttiene un constructor predeterminado, definido por el compilador, hana::tupletiene uno definido por el usuario. Además, dado que hay una especialización para tuplas vacías, que no tiene ningún constructor, funciona el mismo código para tuplas vacías: godbolt.org/z/ZeEVQN
Michał Łoś
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.