Como resolver o problema de “leitura da variável não constexpr 'a' não é permitida em uma expressão constante” com boost.hana

8

Eu estou usando c ++ 17 com Boost.hana para escrever alguns programas de meta-programação. Uma questão que me surpreendeu é que tipo de expressão pode ser usada em um contexto constexpr como static_assert. Aqui está um exemplo:

#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);
    }
}

Primeiro, escrevo uma classe X com dados de campo e um acessador getData () . No main () 's test1 parte, x1.data e x1.getData () se comportam mesmo que eu esperava. Mas na parte test2 , alterar o argumento para uma tupla boost :: hana static_assert(x2.data[0_c] == 1_c)ainda se comporta bem, mas static_assert(x2.getData()[0_c] == 1_c)falha na compilação, com o erro de ' leitura da variável não constexpr' x2 'não é permitido em uma expressão constante '. O que weired é se eu dividir x2.getData()[0_c]em auto data = x2.getData();e static_assert(data[0_c] == 1_c);ele compila bem novamente. Eu esperava que eles se comportassem da mesma maneira. Então, alguém pode ajudar a explicar por x2.getData()[0_c]que não pode ser usado em static_assert neste exemplo?

Para reproduzir: clang ++ 8.0 -I / caminho / para / hana-1.5.0 / include -std = c ++ 17 Test.cpp

Longo
fonte
Curiosamente, o GCC o compila com sucesso . Eu reduzi o código para este mais curto .
xskxzr 28/02
constexprestá faltando no x2e data, constem getData. godbolt.org/z/ZNL2BK
Maxim Egorushkin
@xskxzr Infelizmente, tenho apenas o Clang para usar no meu alvo.
Long

Respostas:

5

O problema é que boost::hana::tuplenão possui um construtor de cópias.

Ele tem um construtor que se parece com um construtor de cópia:

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_)
{ }

Mas, como esse é um modelo, não é um construtor de cópias .

Desde boost::hana::tuplenão tem um construtor de cópia, um é declarado implicitamente e definidos como inadimplentes (que não é suprimida uma vez boost::hana::tuplenão tem nenhum Copiar ou mover construtores ou operadores de atribuição, porque, você adivinhou-lo, eles não podem ser modelos).

Aqui vemos divergência de implementação , demonstrada no comportamento do seguinte 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);
}

O gcc aceita, enquanto Clang e MSVC rejeitam, mas aceitam se a linha não #1é comentada. Ou seja, os compiladores discordam sobre se o construtor de cópias definido implicitamente de uma classe não (diretamente) vazia é permitido para uso no contexto de avaliação constante.

De acordo com a definição do construtor de cópias definido implicitamente, não há como o # 1 ser diferente do que constexpr A(A const&) = default;o gcc esteja correto. Observe também que, se dermos a B um construtor de cópias constexpr definido pelo usuário, Clang e MSVC novamente aceitarem, o problema parece ser que esses compiladores não conseguem rastrear a construtibilidade da cópia constexpr de classes copiáveis ​​implicitamente copiáveis ​​recursivamente vazias. Erros arquivados para MSVC e Clang ( corrigidos para Clang 11).

Observe que o uso de operator[]é um arenque vermelho; a questão é se os compiladores permitem a chamada getData()(que cópia constrói T) dentro de um contexto de avaliação constante como static_assert.

Obviamente, a solução ideal seria o Boost.Hana corrigir de boost::hana::tupleforma que ele tenha construtores de copiar / mover reais e operadores de atribuição de copiar / mover. (Isso resolveria seu caso de uso, pois o código estaria chamando construtores de cópia fornecidos pelo usuário, que são permitidos no contexto de avaliação constante.) Como solução alternativa , você pode considerar getData()a possibilidade de hackear para detectar o caso de não-stateful T:

constexpr T getData() {
    if (data == T{})
        return T{};
    else
        return data;
}
ecatmur
fonte
Não entendo por que o construtor de cópias padrão não funciona ... Você pode tentar explicá-lo novamente para mim?
Antoine Morrier
@AntoineMorrier Eu acredito que isso seja um bug no clang; parece ter dificuldade em reconhecer que classes recursivamente vazias podem ser copiadas no contexto constexpr.
ecatmur
Obrigado. Resposta muito útil! Então, você pode explicar melhor por que a test2.2parte aceita por Clang? (Editei a pergunta original e dividi o teste2 em teste2.1 e teste2.2) Eu esperava que eles se comportassem da mesma forma.
Long
@ O longo período em que Clang está insatisfeito é com a construção de cópias hana::tupleque ocorre no retorno de getData. No teste2.2, a cópia ocorre fora do contexto de avaliação constante; portanto, Clang está bem com ela.
ecatmur 3/03
er .. é um pouco difícil, pelo menos para mim entender isso .. getData()não é permitido aqui, mas sair e introduzindo um temperal então aceita ..
Longo
1

O problema é que você está tentando recuperar um valor de tempo de execução e testá-lo na compilação.

O que você pode fazer é forçar a expressão em tempo de compilação por meio de um decltypee ele funcionará como um 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);
    }
}

Agora, a expressão é avaliada em tempo de compilação, portanto, o tipo é conhecido em tempo de compilação e, como também é construtível em tempo de computação, é possível usá-la em um static_assert

Antoine Morrier
fonte
Obrigado pela resposta. Entendo que o uso static_assert(decltype(x2.getData()[0_c]){} == 1_c)pode funcionar bem, mas ainda quero salvar o arquivo decltypeporque isso pouparia muito. Eu acho que você está dizendo que x2.getData()está recuperando um valor de tempo de execução para que ele não apareça em uma expressão static_assert. Então não entendo por que a x1.getData()parte in test1 e os dados da data[0_c]parte test2 podem funcionar bem. Quais são as diferenças deles?
Long
Realmente, não há nenhum valor em tempo de execução sendo acessado, mas talvez o padrão não permita que o compilador verifique isso.
Jason Rice
0

Então, primeiro de tudo, você está perdendo o qualificador const no getData()método, então deve ser:

constexpr T getData() const

Nenhuma variável é promovida, pelo menos do ponto de vista padrão, para ser constexpr se não estiver marcada como constexpr.

Observe que isso não é necessário para o x1tipo Xespecializado em hana :: integral_constant, pois result of 1_cé um tipo sem construtor de cópias definido pelo usuário, que não contém nenhum dado internamente, portanto, uma operação de cópia getData()é de fato uma operação não operacional. , então expression: static_assert(x1.getData() == 1_c); está correto, pois não há uma cópia real (não é necessário acesso ao thisponteiro não const de x1).

Isso é muito diferente para o contêiner com o hana::tuplequal contém a construção de cópias factuais dos hana::tupledados no x2.datacampo. Isso requer acesso factual ao seu thisponteiro - o que não era necessário x1, mas também não era uma variável constexpr.

Isso significa que você está expressando sua intenção errada com ambos x1e x2, e é necessário, pelo menos para x2, marcar essas variáveis ​​como constexpr. Observe também que o uso da tupla vazia, que é uma especialização geral basicamente vazia (sem construtores de cópias definidas pelo usuário) hana::tuple, funciona perfeitamente (seção 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());
    }
}
Michał Łoś
fonte
Ainda não li toda a resposta, mas um método constexpr pode ser não const normalmente.
Antoine Morrier
Eu discordo que x1é um tipo vazio. Qualquer instância de Xpossui um membro de dados. Também hana::tupleconter tipos vazios também é vazio, pois usa a otimização de base vazia. Você pode estar no alvo culpando o construtor de cópias, porque Clang ou libc ++ podem estar fazendo algo complicado std::integral_constant.
Jason Rice
E, existe uma maneira de não precisar adicionar um constexpr para a declaração x2? Eu gostaria que o X pudesse ser inicializado com valor constante e valor de tempo de execução. Tais como: `` `int g = 1; int main () {{/ * test3 * / auto x3 = X (g); }} `` `Espero que também funcione perfeitamente. Mas adicionar um constexpr a x3 não será compilado, com erro:constexpr variable 'x3' must be initialized by a constant expression
Long
@AntoineMorrier: Sim, mas tudo bem se você não estiver usando o const thisponteiro e, infelizmente, o estiver usando x2no caso static_assert. (no caso de x1 - é uma discussão mais aprofundada:)).
Michał Łoś
@ JasonRice: Sim, isso é verdade, vou ajustar a resposta, pois não fui preciso: é claro que ambos não têm campos não estáticos. Ainda assim, e é isso que falta em minha resposta, observe que, embora hana::integral_constanttenha um construtor padrão definido pelo compilador, hana::tupleele possui um definido pelo usuário. Além disso, como existe uma especialização para tupla vazia, que não possui construtor, o mesmo código para tupla vazia funciona: godbolt.org/z/ZeEVQN
Michał Łoś