construtor constexpr não chamado como constexpr para conversão implícita de tipo

Aug 25 2020

Eu fiz algum código que é capaz de despachar para uma função com base no call-site fornecendo uma string associada a uma determinada função (por meio tuplede ponteiros de função e uma matriz paralela). Em vez de aceitar uma string diretamente, a função dispatch aceita um Callabletipo, onde a const char*é conversível em a Callable.

O construtor de Callableé constexpr, e procura uma função do notado tuplecom uma pesquisa recursiva básica. Eu verifiquei que o construtor é capaz de funcionar corretamente e criar um constexpr Callable(exemplo incluído). Como a função dispatch recebe os argumentos para passar para o Callable's operator(), eu sei a assinatura da função esperada do Callable's operator()no momento em que a crio.

Estou tentando realizar duas verificações em tempo de compilação, quando elas podem ser feitas em tempo de compilação. Primeiro, verifico se a string fornecida existe na matriz predefinida de strings. Em segundo lugar, verifico se a assinatura da função associada a essa string corresponde à assinatura esperada tupledos ponteiros de função. Eu crio mensagens de erro "amigáveis" em tempo de compilação fazendo throw()'ing' dentro do constexprmétodo que procura a função.

Verifiquei que, ao criar um constexprcallable, recebo as mensagens de erro esperadas em tempo de compilação. Isso funciona. O que não funciona é receber mensagens em tempo de compilação se eu usar my Dispatcherdiretamente, permitindo que o call-site converta uma string em um arquivo Callable. Eu sei que quando eu uso parâmetros de tempo de execução, minha função dispatch não será chamada em um constexprcontexto - eu intencionalmente não fiz essa função constexpr; o objetivo é chamá-lo com valores de tempo de execução. Mas pensei que as conversões implícitas "acontecem no local da chamada" , não dentro da função chamada.

Portanto, pensei que em uma chamada como dispatcher("one", 1)(que chama a primeira função com um parâmetro de 1) ficaria assim: "one" é convertido em a Callable no call-site , então uma chamada é feita como dispatcher(Callable("one"), 1). Isso significaria que o constexprconstrutor poderia ser usado, pelo menos. Na minha experiência, desde que você não ignore o resultado de uma constexprchamada, a chamada é feita como constexprse pudesse, caso contrário, é feita como tempo de execução. Consulte Funções Constexpr não chamadas em tempo de compilação se o resultado for ignorado . Isso não está acontecendo - o construtor de conversão está sendo chamado em tempo de execução quando a conversão ocorre em uma chamada para minha função de despacho!

Alguém conhece uma maneira de alterar meu código para que o construtor de conversão seja chamado em tempo de compilação, se puder ??? Encontrei uma solução totalmente diferente para resolver essa classe geral de problema neste post , mas, francamente, gosto mais da sintaxe do código abaixo, se pudesse fazê-lo funcionar.

Não vou incluir o código acima no corpo desta postagem, mas sim um exemplo mais canônico que demonstra o comportamento e também mostra o comportamento que vi na postagem que mencionei acima, tudo em um.

Demonstração ao vivo do abaixo:https://onlinegdb.com/r1s1OE77v

Demonstração ao vivo do meu problema "real", se estiver interessado:https://onlinegdb.com/rJCQ2bGXw

Primeiro os "acessórios de teste":

// Modified from https://stackoverflow.com/a/40410624/12854372

// In a constexpr context, ContextIsConstexpr1(size_t) always
// simply sets _s to 1 successfully.

extern bool no_symbol_s_is_zero;

struct ContextIsConstexpr1 {
    size_t _s;

    constexpr ContextIsConstexpr1(size_t s) : _s(s ? 1 : no_symbol_s_is_zero) {}
};

// In a constexpr context, ContextIsConstexpr2(size_t) will cause
// a compile-time error if 0 is passed to the constructor

struct ContextIsConstexpr2 {
    size_t _s;

    constexpr ContextIsConstexpr2(size_t s) : _s(1) {
        if(!s) {
            throw logic_error("s is zero");
        }
    }
};

// Accept one of the above. By using a CONVERSION constructor
// and passing in a size_t parameter, it DOES make a difference.

ContextIsConstexpr1 foo(ContextIsConstexpr1 c) { return c; }
ContextIsConstexpr2 bar(ContextIsConstexpr2 c) { return c; }

Agora o código de teste:

int main()
{
    constexpr size_t CONST = 1;
    #define TEST_OBVIOUS_ONES false
    
    // ------------------------------------------------------------
    // Test 1: result is compile-time, param is compile-time
    // ------------------------------------------------------------

    #if TEST_OBVIOUS_ONES
    
    // Compile-time link error iif s==0 w/ any optimization (duh)
    constexpr auto test1_1 = ContextIsConstexpr1(CONST);
    cout << test1_1._s << endl;

    // Compile-time throw iif s==0 w/ any optimization (duh)
    constexpr auto test1_2 = ContextIsConstexpr2(CONST);
    cout << test1_2._s << endl;

    #endif

    // ------------------------------------------------------------
    // Test 2: result is runtime, param is compile-time
    // ------------------------------------------------------------

    // Compile-time link error iif s==0 w/ any optimization ***See below***
    auto test2_1 = ContextIsConstexpr1(CONST);
    cout << test2_1._s << endl;

    // Runtime throw iif s==0 w/ any optimization
    // NOTE: Throw behavior is different than extern symbol behavior!!
    auto test2_2 = ContextIsConstexpr2(CONST);
    cout << test2_2._s << endl;

    // ------------------------------------------------------------
    // Test 3: Implicit conversion
    // ------------------------------------------------------------

    // Compile-time link error if (1) s==0 w/ any optimization *OR* (2) s>0 w/ low optimization!!
    // Note: New s>0 error due to implicit conversion ***See above***
    auto test3_1 = foo(CONST);
    cout << test3_1._s << endl;

    // Runtime throw iif s==0 w/ any optimization
    auto test3_2 = bar(CONST);
    cout << test3_2._s << endl;

    // ------------------------------------------------------------
    // Test 4: result is ignored, param is compile-time
    // ------------------------------------------------------------

    // Compile-time link error w/ any 's' iif low optimization
    // Note: no error w/ s==0 with high optimization, new error w/ s>0 by ignoring result ***See above***
    ContextIsConstexpr1{CONST};

    // Runtime throw iif s==0 w/ any optimization
    ContextIsConstexpr2{CONST};

    // ------------------------------------------------------------
    // Get runtime input, can't optimize this for-sure
    // ------------------------------------------------------------

    #if TEST_OBVIOUS_ONES

    size_t runtime;
    cout << "Enter a value: ";
    cin >> runtime;

    // ------------------------------------------------------------
    // Test 5: result is runtime, param is runtime
    // ------------------------------------------------------------

    // Compile-time link error w/ any 's' w/ any optimization (duh)
    auto test5_1 = ContextIsConstexpr1(runtime);
    cout << test5_1._s << endl;

    // Runtime throw iif s==0 w/ any optimization (duh)
    auto test5_2 = ContextIsConstexpr2(runtime);
    cout << test5_2._s << endl;

    // ------------------------------------------------------------
    // Test 6: result is ignored, param is runtime
    // ------------------------------------------------------------

    // Compile-time link error w/ any 's' w/ any optimization (duh)
    ContextIsConstexpr1{runtime};

    // Runtime throw iif s==0 w/ any 's' w/ any optimization (duh)
    ContextIsConstexpr2{runtime};

    #endif
}

Respostas

Jarod42 Aug 25 2020 at 14:44

Alguém sabe de uma maneira de alterar meu código para que o construtor de conversão seja chamado em tempo de compilação, se puder ser

Como eu disse no link postado, a chamada de constexprfunções em tempo de compilação é feita apenas na expressão constante.

Os parâmetros não são constexpr.

Uma solução alternativa seria usar MACRO:

#define APPLY_DISPATCHER(dispatcher, str, ...) \
    do { \
        constexpr callable_type_t<decltype(dispatcher),  decltype(make_tuple(__VA_ARGS__))> callable(str); \
        (dispatcher)(callable, __VA_ARGS__); \
    } while (0)

com

template <typename Dispatcher, typename Tuple> struct callable_type;

template <typename Dispatcher, typename ... Ts>
struct callable_type<Dispatcher, std::tuple<Ts...>>
{
    using type = typename Dispatcher::template Callable<Ts...>;
};

template <typename Dispatcher, typename Tuple> 
using callable_type_t = typename callable_type<Dispatcher, Tuple>::type;

Com uso:

APPLY_DISPATCHER(dispatcher, "one", 1);
APPLY_DISPATCHER(dispatcher, "a", 1); // Fail at compile time as expected

Demo .

Mas não muito melhor do que o proposto dispatcher.dispatch(MAKE_CHAR_SEQ("a"), 1);(ou com extensão dispatcher.dispatch("a"_cs, 1);) (fornecendo sobrecarga de despacho para poder criar constexpr Callable).