constexpr, static_assert, and inlining
Asked Answered
W

3

14

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.

Whether answered 23/4, 2012 at 5:35 Comment(5)
it is possible to write a macro that determines whether an expression is constant in standard c++Tiphani
@JohannesSchaub-litb Wouldn't such a macro need to expand to a class definition? That wouldn't be usable inside a constexpr function.Aquilegia
@pot why would it need to expand to a class definition?Tiphani
@JohannesSchaub-litb the implementation I'm thinking of would use type traits style SFINAE with a class containing a function template and a fallback. Although, on second thought, it would run afoul of the rule that every template must have a potential valid instantiation.Aquilegia
@JohannesSchaub-litb: Could you point me in the right direction for how I would do that? Everything I can think of halts compilation if it's not.Whether
T
9

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))
Tiphani answered 23/4, 2012 at 20:15 Comment(3)
visual c++ doesn't yet support constexpr ( as of sep 2012, with visual c++ 11.0). thus, as yet it's a bit impractical...Devilfish
Very nice Johannes. Why does it work? Is every constexpr an rvalue reference? Why the name? What is pr? make?val? Does noexcept return a bool? Ok, I'll look up noexcept, but any elaboration would be gratefully received. There's an upvote in it for you ;)Iphlgenia
This could really use an explanation of how it works. I am trying to do something related to the original question and I can't tell how your snippet is supposed to be used, so I can't adapt it to my situation.Demagnetize
D
7

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.

Devilfish answered 23/4, 2012 at 6:26 Comment(2)
g++ has aggressive constant folding right into the parser/semantic analysis, meaning that (e)-(e) is immediately evaluated to 0 and thus deemed usable as a null pointer value... even though semantically it is an int. Clang however will not fold the x case, and an int cannot be implicitly converted to a pointer value, so x is not.Gallic
@MatthieuM.: yes, detecting that is the whole point here, what the code is designed to do. i don't understand why people have upvoted your comment, unless they're Reddit kids. it's inane.Devilfish
A
0

How is the answer to the other question disappointing? It implements almost exactly what you presently describe, except for the way the compiler prints the text in the diagnostic message.

The reason it needs to be done with throw is that compile-time evaluation of constexpr in a context that could be evaluated at runtime is optional. For example, the implementation could choose to let you step through the constexpr code in debugging mode.

constexpr is a weak attribute of functions (declaration specifier) that cannot change the resulting value of an expression using the function. It is a guarantee that the semantic meaning at runtime is fixed at compile time, but doesn't allow you to specify a special compile-time shortcut.

As for flagging invalid conditions, throw is a subexpression which is invalid as a constant expression, except when hidden in the unevaluated side of ?:, &&, or ||. The language guarantees that this will be flagged at compile time, even if the debugger lets you step through it at runtime, and only if the flag is really triggered.

The language gets things right here. Unfortunately that cannot be reconciled with the special diagnostic message feature of static_assert or branching on constexpr-ness.

Aquilegia answered 23/4, 2012 at 9:13 Comment(1)
I must be misunderstanding something then, because I don't understand how any other answer has solved my problem. I don't see how using throw solves my problem, either, because I still cannot use static_assert here, which is the entire point. Without static_assert, all I have is a normal assert / throw.Whether

© 2022 - 2024 — McMap. All rights reserved.