As funções Constexpr não são chamadas em tempo de compilação se o resultado for ignorado

Aug 15 2020

Eu estava investigando alguns resultados bastante estranhos de cobertura de código de constexprfunções (as chamadas em tempo de compilação não podem ser instrumentadas pela ferramenta de cobertura de código que uso) e notei que algumas constexprfunções estavam sendo chamadas como funções de tempo de execução , se os resultados da função chamada não foram armazenados .

Parece que, para constexprfunções ou métodos, se você armazenar os resultados da chamada (seja em uma variável de tempo de execução [ênfase!!!] ou em uma constexprvariável), a chamada será em tempo de compilação (desde que os parâmetros sejam compilados -Tempo). Se você ignorar os resultados, a chamada será uma chamada de tempo de execução. Isso não tem nada a ver com minha ferramenta de cobertura de código; o comportamento parece ser repetível no exemplo simples abaixo.

Você poderia argumentar que, como constexpras funções não podem ter efeitos colaterais, não importa o que o compilador faça se você não retornar/usar um resultado. Eu acho que, para eficiência, o compilador ainda faria o que puder ser constexpr, mas isso não está aqui nem ali ... O que estou querendo saber é se esse é um comportamento definido.

Esta é uma maneira portátil de garantir que suas constexprfunções sejam invocadas como tempo de execução??? Não há muitos usos para isso, mas um uso é: se você quiser "receber crédito por testes que foram chamados em constexprfunções" na cobertura de código, apenas chamando a mesma função no final de seu teste de unidade e ignorando o resultado, para que sejam instrumentados.

Existem outras maneiras de forçar uma função a ser chamada como tempo de execução, eu sei, estou mais curioso sobre o que está acontecendo aqui. Foi muito inesperado quando o vi pela primeira vez. A menos que seja portátil, provavelmente chamarei meus constexprmétodos por meio de um objeto de tempo de execução (o que parece funcionar mesmo para métodos estáticos) ou por meio de um ponteiro de função de tempo de execução.

Veja o exemplo abaixo. Demonstração ao vivo:https://onlinegdb.com/rJao0RNGP

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

extern bool no_symbol;

struct ContextIsConstexpr {
    size_t s;

    constexpr ContextIsConstexpr() : s(1) {}
    
    constexpr void operator()() {
        auto ignore = s ? 1 : no_symbol;
    }
};

constexpr bool tryIgnoringResult()
{
    ContextIsConstexpr()();
    return true;
}

constexpr void thereIsNoResult() {
    ContextIsConstexpr()();
}

int main()
{
    constexpr auto result1 = tryIgnoringResult(); // OK, compile-time
    auto result2 = tryIgnoringResult(); // OK, compile-time

    // tryIgnoringResult(); // Unexpected, runtime!
    // thereIsNoResult(); // Unexpected, runtime!
}

Respostas

Jarod42 Aug 15 2020 at 14:03

Pode ser confuso, mas as funções constexpr devem ser chamadas em tempo de compilação apenas em contextos constexpr (atribuição à variável constexpr, uso para tamanho de array ou parâmetro de modelo, ...).

No contexto regular, as funções são chamadas em tempo de execução. O Optimizer pode resolver essa função em tempo de compilação (como para qualquer outra função seguindo a regra como se). constexpré de fato uma boa dica para que a otimização aconteça.

Você poderia argumentar que, como as funções constexpr não podem ter efeitos colaterais

Eles podem ter efeitos colaterais, veja o seguinte exemplo válido:

constexpr int f(int i)
{
    if (i == 0) return 0;
    std::cout << i << std::endl;
    return i;
}

int main()
{
    [[maybe_unused]] constexpr int zero = f(0); // Compile time
    f(42); // runtime
}

Demonstração