Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

constexpr, static_assert, and inlining

I previously asked about function overloading based on whether the arguments are constexpr. I'm trying to work around the disappointing answer to that question to make a smarter assert function. This is roughly what I am trying to do:

inline void smart_assert (bool condition) {
    if (is_constexpr (condition))
        static_assert (condition, "Error!!!");
    else
        assert (condition);
}

Basically, the idea is that a compile-time check is always better than a run-time check if it's possible to check at compile time. However, due to things like inlining and constant folding, I can't always know whether a compile time check is possible. This means that there may be cases where assert (condition) compiles down to assert(false) and the code is just waiting for me to run it and execute that path before the I find out there is an error.

Therefore, if there were some way to check whether the condition is a constexpr (due to inlining or other optimizations), I could call static_assert when possible, and fall back on a run-time assert otherwise. Fortunately, gcc has the intrinsic __builtin_constant_p (exp), which returns true if exp is a constexpr. I don't know if other compilers have this intrinsic, but I was hoping that this would solve my problem. This is the code that I came up with:

#include <cassert>
#undef IS_CONSTEXPR

#if defined __GNUC__
    #define IS_CONSTEXPR(exp) __builtin_constant_p (exp)
#else
    #define IS_CONSTEXPR(exp) false
#endif
// TODO: Add other compilers

inline void smart_assert (bool const condition) { 
    static_assert (!IS_CONSTEXPR(condition) or condition, "Error!!!");
    if (!IS_CONSTEXPR(condition))
        assert (condition);
}

#undef IS_CONSTEXPR

The static_assert relies on the short circuit behavior of or. If IS_CONSTEXPR is true, then static_assert can be used, and the condition is !true or condition, which is the same as just condition. If IS_CONSTEXPR is false, then static_assert cannot be used, and the condition is !false or condition, which is just the same as true and the static_assert is ignored. If the static_assert cannot be checked because condition is not a constexpr, then I add a run-time assert to my code as a last-ditch effort. However, this does not work, thanks to not being able to use function arguments in a static_assert, even if the arguments are constexpr.

In particular, this is what happens if I try to compile with gcc:

// main.cpp
int main () {
    smart_assert (false);
    return 0;
}

g++ main.cpp -std=c++0x -O0

Everything is fine, compiles normally. There is no inlining with no optimization, so IS_CONSTEXPR is false and the static_assert is ignored, so I just get a run-time assert statement (that fails). However,

[david@david-desktop test]$ g++ main.cpp -std=c++0x -O1
In file included from main.cpp:1:0:
smart_assert.hpp: In function ‘void smart_assert(bool)’:
smart_assert.hpp:12:3: error: non-constant condition for static assertion
smart_assert.hpp:12:3: error: ‘condition’ is not a constant expression

As soon as I turn on any optimizations and thus potentially allow static_assert to be triggered, it fails because I cannot use function arguments in the static_assert. Is there some way to work around this (even if it means implementing my own static_assert)? I feel my C++ projects could theoretically benefit quite a bit from a smarter assert statement that catches errors as early as possible.

It doesn't seem like making smart_assert a function-like macro will solve the problem in the general case. It will obviously make it work in this simple example, but condition may have come from a function two levels up the call graph (but still becomes known to the compiler as a constexpr due to inlining), which runs into the same problem of using a function parameter in a static_assert.

like image 259
David Stone Avatar asked Apr 23 '12 05:04

David Stone


People also ask

What is static_assert?

static_assert is a keyword defined in the <assert. h> header. It is available in the C11 version of C. static_assert is used to ensure that a condition is true when the code is compiled. The condition must be a constant expression.

Is Constexpr evaluated?

The constexpr specifier declares that it is possible to evaluate the value of the function or variable at compile time.

Does Constexpr improve performance?

Understanding constexpr Specifier in C++ constexpr is a feature added in C++ 11. The main idea is a performance improvement of programs by doing computations at compile time rather than run time.

Can Constexpr throw?

Even though try blocks and inline assembly are allowed in constexpr functions, throwing exceptions or executing the assembly is still disallowed in a constant expression.


2 Answers

This should help you start

template<typename T> 
constexpr typename remove_reference<T>::type makeprval(T && t) {
  return t;
}

#define isprvalconstexpr(e) noexcept(makeprval(e))
like image 114
Johannes Schaub - litb Avatar answered Oct 10 '22 05:10

Johannes Schaub - litb


Explicit is good, implicit is bad, in general.

The programmer can always try a static_assert.

If the condition can not be evaluated at compile time, then that fails, and the programmer needs to change to assert.

You can make it easier to do that by providing a common form so that the change reduces to e.g. STATIC_ASSERT( x+x == 4 )DYNAMIC_ASSERT( x+x == 4 ), just a renaming.

That said, since in your case you only want an optimization of the programmer’s time if that optimization is available, i.e. since you presumably don’t care about getting the same results always with all compilers, you could always try something like …

#include <iostream>
using namespace std;

void foo( void const* ) { cout << "compile time constant" << endl; }
void foo( ... ) { cout << "hm, run time,,," << endl; }

#define CHECK( e ) cout << #e << " is "; foo( long((e)-(e)) )

int main()
{
    int x   = 2134;
    int const y     = 2134;

    CHECK( x );
    CHECK( y );
}

If you do, then please let us know how it panned out.

Note: the above code does produce different results with MSVC 10.0 and g++ 4.6.


Update: I wondered how the comment about how the code above works, got so many upvotes. I thought maybe he's saying something I simply don't understand. So I set down to do the OP's work, checking how the idea fared.

At this point I think that if the constexpr function thing can be be made to work with g++, then it's possible to solve the problem also for g++, otherwise, only for other compilers.

The above is as far as I got with g++ support. This works nicely (solves the OP's problem) for Visual C++, using the idea I presented. But not with g++:

#include <assert.h>
#include <iostream>
using namespace std;

#ifdef __GNUC__
    namespace detail {
        typedef double (&Yes)[1];
        typedef double (&No)[2];

        template< unsigned n >
        Yes foo( char const (&)[n] );

        No foo( ... );
    }  // namespace detail

    #define CASSERT( e )                                        \
        do {                                                    \
            char a[1 + ((e)-(e))];                              \
            enum { isConstExpr = sizeof( detail::foo( a ) ) == sizeof( detail::Yes ) }; \
            cout << "isConstExpr = " << boolalpha << !!isConstExpr << endl; \
            (void)(isConstExpr? 1/!!(e) : (assert( e ), 0));    \
        } while( false )
#else
    namespace detail {
        struct IsConstExpr
        {
            typedef double (&YesType)[1];
            typedef double (&NoType)[2];

            static YesType check( void const* );
            static NoType check( ... );
        };
    }  // namespace detail

    #define CASSERT( e )                                            \
        do {                                                        \
            enum { isConstExpr =                                    \
                (sizeof( detail::IsConstExpr::check( e - e ) ) ==   \
                    sizeof( detail::IsConstExpr::YesType )) };      \
            (void)(isConstExpr? 1/!!(e) : (assert( e ), 0));        \
        } while( false )
#endif

int main()
{
#if defined( STATIC_TRUE )
    enum { x = true };
    CASSERT( x );
    cout << "This should be displayed, OK." << endl;
#elif defined( STATIC_FALSE )
    enum { x = false };
    CASSERT( x );
    cerr << "!This should not even have compiled." << endl;
#elif defined( DYNAMIC_TRUE )
    bool x = true;
    CASSERT( x );
    cout << "This should be displayed, OK." << endl;
#elif defined( DYNAMIC_FALSE )
    bool x = false;
    CASSERT( x );
    cout << "!Should already have asserted." << endl;
#else
    #error "Hey, u must define a test case symbol."
#endif
}

Example of the problem with g++:

[D:\dev\test]
> g++ foo.cpp -Werror=div-by-zero -D DYNAMIC_FALSE

[D:\dev\test]
> a
isConstExpr = true
!Should already have asserted.

[D:\dev\test]
> _

That is, g++ reports (even via its intrinsic function, and even wrt. creating VLA or not) that a non- const` variable that it knows the value of, is constant, but then it fails to apply that knowledge for integer division, so that it then fails to produce warning.

Argh.


Update 2: Well I'm dumb: of course the macro can just add an ordinary assert to have there in any case. Since the OP is only interested in getting the static assert when it's available, which isn't for g++ in some corner cases. Problem solved, and was solved originally.

like image 37
Cheers and hth. - Alf Avatar answered Oct 10 '22 05:10

Cheers and hth. - Alf