Como despachar entre assert () e static_assert (), depende se no contexto constexpr?

8

Nas funções constexpr do C ++ 11, uma segunda instrução como uma assert()não é possível. A static_assert()é bom, mas não funcionaria se a função fosse chamada como função 'normal'. O operador de vírgula pode vir para ajudar. o assert(), mas é feio e algumas ferramentas cospem avisos sobre ele.

Considere esse 'getter' que é perfeitamente possível ao lado da afirmação. Mas eu gostaria de manter algum tipo de asserção para tempo de execução e tempo de compilação, mas não posso simplesmente sobrecarregar, dependendo do contexto 'constexpr'.

template<int Size>
struct Array {
  int m_vals[Size];
  constexpr const int& getElement( int idx ) const
  {
    ASSERT( idx < Size ); // a no-go for constexpr funcs in c++11
    // not possible, even in constexpr calls as being pointed out, but what I would like:
    static_assert( idx < Size, "out-of-bounds" );
    return m_vals[idx];
  }
};

Condições secundárias: C ++ 11, sem heap, sem exceções, sem detalhes específicos do compilador.

Observe como os comentaristas apontaram (obrigado!), static_assertO argumento não é possível (mas seria bom). O compilador me deu um erro diferente no acesso fora dos limites nessa situação.

Borph
fonte
" mas é feio " Bem, o operador de vírgula pode ser feio, mas faz o trabalho no C ++ 11. A solução mais sofisticada seria mudar para o C ++ 14. :)
Acorn
" algumas ferramentas cospem avisos sobre ele " Quais ferramentas e quais avisos?
Acorn
O operador de vírgula costumava ser minha solução, mas a) é avisado pela análise de código estático como qacpp (diretrizes de codificação) eb) estava levando a um erro de sintaxe estranho em um projeto a jusante (não entendeu, suspeite de uma macro de afirmação personalizada) . Bem, eu apenas tento evitá-lo agora, mas concordo que fez o trabalho.
Borph 23/01
2
@Borph Não, você não pode usar static_assertdependente idxem tudo. Você só pode diagnosticar um valor incorreto idxse a função for usada em um contexto que exija uma expressão constante, forçando a avaliação de uma construção que a torna não uma expressão constante. Fora desse contexto, você nunca pode verificar o valor em tempo de compilação.
walnut

Respostas:

2

Algo como

void assert_impl() { assert(false); } // Replace body with own implementation

#ifdef NDEBUG // Replace with own conditional
#define my_assert(condition) ((void)0)
#else
#define my_assert(condition) ((condition) ? (void()) : (assert_impl(), void()))
#endif

template<int Size>
struct Array {
  int m_vals[Size];
  constexpr const int& getElement( int idx ) const
  {
    return my_assert(idx < Size), m_vals[idx];
  }
};

Ele fornecerá um erro de tempo de compilação na falha de asserção, se usado em um contexto que exija uma expressão constante (porque chamará uma não- constexprfunção).

Caso contrário, ele falhará em tempo de execução com uma chamada para assert(ou seu analógico).

É o melhor que você pode fazer, até onde eu sei. Não há como usar o valor de idxpara forçar uma verificação em tempo de compilação fora do contexto, exigindo expressões constantes.

A sintaxe do operador de vírgula não é boa, mas as constexprfunções do C ++ 11 são muito limitadas.

Obviamente, como você já observou, o comportamento indefinido será diagnosticado de qualquer maneira se a função for usada em um contexto que exija uma expressão constante.

Se você sabe que assert(ou o seu análogo) não se expande para algo proibido em uma expressão constante, se a condição é avaliada, truemas o faz se for avaliado false, você pode usá-lo diretamente em vez de my_assertpular a indireta que eu construo no meu código

noz
fonte
1
Dados os votos negativos, alguém poderia me explicar onde minha resposta está errada?
walnut
Essa solução e a @ecatmur são semelhantes, só posso escolher uma resposta. O seu é direto. Uma observação: por que (void)0no NDEBUGcaso e void()no outro? Ou é realmente o mesmo?
Borph 24/01
O @Borph (void)0é um no-op, ele não compila nada (que é o que você deseja quando NDEBUGé definido), enquanto você precisa do void()para que o segundo e o terceiro operandos do operador condicional tenham o mesmo tipo void,.
Bob__ 24/01
@ Borph Eu acho (void)0que seria bom em todos os casos também. Acabei de substituí-lo no primeiro caso, porque void()também pode ser analisado como tipo de função sem parâmetros e sem tipo de retorno, dependendo do contexto. Não pode ser analisado dessa maneira nas subexpressões no segundo caso.
noz
3

Melhor que uma expressão de vírgula, você pode usar uma condicional ternária. O primeiro operando é o seu predicado de asserção, o segundo operando é a sua expressão de sucesso e, como o terceiro operando pode ser qualquer expressão - mesmo que não seja utilizável em um contexto constante de C ++ 11 - você pode usar um lambda para chamar as ASSERTinstalações da sua biblioteca :

#define ASSERT_EXPR(pred, success)    \
    ((pred) ?                         \
     (success) :                      \
     [&]() -> decltype((success))     \
     {                                \
         ASSERT(false && (pred));     \
         struct nxg { nxg() {} } nxg; \
         return (success);            \
     }())

Explicação do corpo da lambda:

  • ASSERT(false && (pred)) é garantir que seu mecanismo de asserção seja chamado com uma expressão apropriada (para stringification).
  • struct nxg { nxg() {} } nxgé para segurança futura, para garantir que, se você compilar em C ++ 17 ou superior com NDEBUGo lambda ainda não for constexpr, a asserção será imposta no contexto de avaliação const.
  • return (success)existe por dois motivos: garantir que o segundo e o terceiro operandos tenham o mesmo tipo e, se a sua biblioteca respeitar, NDEBUGa successexpressão será retornada independentemente pred. ( predserão avaliados , mas você espera que os predicados de afirmação sejam baratos para avaliar e não tenham efeitos colaterais).

Exemplo de uso:

template<int Size>
struct Array {
  int m_vals[Size];
  constexpr int getElement( int idx ) const
  {
    return ASSERT_EXPR(idx < Size, m_vals[idx]);
  }
};

constexpr int I = Array<2>{1, 2}.getElement(1); // OK
constexpr int J = Array<2>{1, 2}.getElement(3); // fails
ecatmur
fonte
Ah! @walnut e uma variável não inicializada podem ser permitidas mais tarde, se não forem acessadas. Vou tentar encontrar uma guarda melhor. Obrigado!
ecatmur 23/01
sugestão: s / [&]/ [&] -> decltype((success))para conservar referências.
LF
@LF bom ponto, obrigado!
ecatmur 24/01
Você está certo de que a predavaliação deve ser barata, mas nem sempre. Então, como uma ASSERT_EXPRmacro geral, eu não recomendaria. Às vezes, faço chamadas caras e me afirmo (por exemplo, para verificar invariantes).
Borph 24/01
1
@ Borph Estou assumindo que, mesmo se você ativar NDEBUGa desativação de declarações de tempo de execução, ainda deseja que as declarações em tempo de compilação sejam verificadas. Tornar o corpo do lambda de caso de falha não constexpré uma maneira de garantir isso - mas tem o custo de avaliar e descartar o predicado no tempo de execução NDEBUG. Caso contrário, você pode definir a macro em NDEBUGapenas return (success);.
ecatmur 24/01
2

static_assertnão pode ser usado aqui. O argumento para uma constexprfunção não é permitido em uma expressão constante. Portanto, não há solução para o seu problema sob as restrições fornecidas.

No entanto, podemos resolver o problema dobrando duas restrições

  1. não usar static_assert(use outros métodos para produzir um diagnóstico em tempo de compilação) e

  2. desconsidere que o operador de vírgula "é feio e algumas ferramentas emitem avisos sobre isso". (Mostrar sua feiúra é uma conseqüência infeliz dos requisitos estritos das constexprfunções do C ++ 11 )

Então, podemos usar um normal assert:

template <int Size>
struct Array {
  int m_vals[Size];
  constexpr const int& getElement(int idx) const
  {
    return assert(idx < Size), m_vals[idx];
  }
};

Em um contexto de avaliação constante, isso emitirá um erro do compilador error: call to non-'constexpr' function 'void __assert_fail(const char*, const char*, unsigned int, const char*)'.

LF
fonte
-2

Isto que funciona para mim para a maioria dos compiladores: https://godbolt.org/z/4nT2ub

template<int Size>
struct Array {
  int m_vals[Size];
  constexpr const int& getElement(int idx) const
  {
    assert(idx < Size);
    return m_vals[idx];
  }
};

Agora static_assertestá obsoleto, pois constexprnão pode conter comportamento indefinido. Portanto, quando você interrompe o compilador de índice de matriz, reporta o erro adequado. Veja aqui .

O problema é assert. É uma macro cuja implementação é indefinida. Se o compilador usar uma função que não é uma, constexprela falhará, mas como você pode ver, três compiladores principais não terão problemas com isso.

Marek R
fonte
1
Isso não funciona no C ++ 11 (um dos requisitos do OP). Os compiladores provavelmente estão usando C ++ 14 ou posterior por padrão.
walnut
com C++11isso falhar somente no gcc godbolt.org/z/DB2zL3
Marek R
Veja as mensagens de aviso. O MSVC não aceita o /std:c++11sinalizador e Clang permite o código, embora exija C ++ 14. Add -pedantic-errorse Clang fornecerão os erros apropriados que um compilador C ++ 11 puro forneceria.
walnut
-3

c ++ 11 não pode ser assim ... idxconstante ou não
seria bom se tivesse uma função cada
. Poderia ser isso se forçado em uma função

template<int Size>
struct Array {
    int m_vals[Size];
    constexpr const  int& getElement( int idx ) const       
    {
    if constexpr(is_same_v<decltype(idx), const int>)
        static_assert( idx < Size, "out-of-bounds" ); // a no-go for non-constexpr calls
    else 
        assert( idx < Size ); // a no-go for constexpr funcs in c++11

    return m_vals[idx];
    }
};

int main() {                // E.g. up to next //

        Array<7> M;  
        int i=M.getElement(1);
}                           //
nonock
fonte
1
Não tenho certeza se isso pretende ser uma resposta para a pergunta ou não, mas seu código não é válido em nenhuma versão do C ++. O problema não é se ou não idxé const. Se essa é apenas uma proposta padrão do C ++, não vejo como ela pertence na seção de respostas.
walnut