Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SFINAE to assert() that code DOES NOT compile

Tags:

c++

sfinae

c++14

I feel certain it must be possible to use SFINAE (possibly with Macros) to static_assert() that arbitary code will not compile.

There are some complex cases in my code-base, where i have a class that I want to forbid taking temporaries (I believe the pattern is):

class(const class&& tmp)=delete; /* deny copying from an unnamed temporary */
class (class&& rhs){/* allow construction from non-temporary rvalue*/}

Currently, I check an undesired constructor DOES NOT compile, but then of course I have to comment it out to get the tests to compile again!

If I could do:

static_assert(DOES_NOT_COMPILE(myclass_t("Hello")));

const char* help = "HELP";
static_assert(!DOES_NOT_COMPILE(myclass_t(help))); 
// COMPILES() might be better here :-)

That would help me a great deal, but I cannot seem to find a general SFINAE solution. C++14 only, so:

if constexpr

is not available.

like image 548
r webby Avatar asked Mar 03 '18 00:03

r webby


2 Answers

The following macro lets you rewrite a SFINAE-unfriendly expression such as [](auto&&x) { return x+1; } in a SFINAE-friendly way.

#define RETURNS(...)\
  noexcept(noexcept(__VA_ARGS__))\
  ->decltype(__VA_ARGS__)\
  { return __VA_ARGS__;}

So that lets you rewrite the above lambda expression like this:

[](auto&&x) RETURNS( x+1 )

or, another example:

struct { template<class X> auto operator()(X&&x) RETURNS(x+1) };

and it is SFINAE friendly. RETURNS isn't actually required, but it makes much of the code so much cleaner. There is a c++20 proposal to replace RETURNS with => by SO's own @barry.

Next we need to be able to test if a function object can be called.

namespace details {
  template<class, class, class...>
  struct can_invoke:std::false_type{};
  template<class F, class...Args>
  struct can_invoke<
    F,
    std::void_t<std::result_of_t<F&&(Args&&...)>>,
    Args...
  >:
    std::true_type
  {};
}
template<class F, class...Args>
using can_invoke=details::can_invoke<F,void,Args...>;

we are almost there. (This is the core of the technique; I sometimes use can_apply that takes template<class...>class Z instead of class F here.) c++17 has a similar trait; it can be used instead.

test_invoke takes callable and returns a callable tester. A callable tester takes arguments, and returns true or false types based on "could the original callable be called with these arguments".

template<class F>
constexpr auto test_invoke(F&&){
  return [](auto&&...args) RETURNS( can_invoke< F, decltype(args)... >{} );
}

and here we are. test_invoke can be skipped if you are willing to work with pure types, but working with values can eliminate some bugs.

auto myclass_ctor=[](auto&&...args)RETURNS(myclass_t(decltype(args)(args)...));

myclass_ctor is a callable object that represents constructing myclass_t.

static_assert(!test_invoke(myclass_ctor)("Hello") );

or

template<class C>
auto ctor=[](auto&&...args)RETURNS(C(decltype(args)(args)...));
static_assert(!test_invoke(ctor<myclass_t>)("Hello") );

this requires constexpr lambda, a c++17 feature but an early one. It can be done without it but it gets ugly. Plus move ctor requirement of elision is annoying to work around in c++14.

To translate to c++14, replace every lambda with a manual function object with appropriate constexpr special member functions. RETURNS applies to operator() just as well, as demonstrated above.

To get around elision move ctor requrement, RETURNS(void( blah )).

Apologies for any tyops; I am on phone.

like image 100
Yakk - Adam Nevraumont Avatar answered Nov 16 '22 00:11

Yakk - Adam Nevraumont


Building on @Yakk's answer, which I find amazing. We can never hope to

static_assert(!DOES_NOT_COMPILE(myclass_t(help))); 

because there must be a type dependency to delay the error, and that's what Yakk is doing. Using another macro, together with default lambda capture:

STATIC_ASSERT_NOT_COMPILES(myclass_t(MK_DEP(help)));

MAKE_DEP is a templated function object, which is injected by the macro to provide the required dependency. Example use:

void foo(){

    std::string s;
    const std::string cs; 

    STATIC_ASSERT_NOT_COMPILES(cs=MK_DEP(s));
    STATIC_ASSERT_NOT_COMPILES(MK_DEP(cs).clear());
    // This fires, because s can be cleared:
    //STATIC_ASSERT_NOT_COMPILES(MK_DEP(s).clear()); // Fails to compile, OK!

    class C{}; // just an example class
    C c;
    STATIC_ASSERT_NOT_COMPILES(c=MK_DEP(7));
    STATIC_ASSERT_NOT_COMPILES(7=MK_DEP(c));
    STATIC_ASSERT_NOT_COMPILES(baz(foo(MK_DEP(7)=c)));
    STATIC_ASSERT_NOT_COMPILES(MK_DEP(false)=1);

    // What about constructing C from string?
    STATIC_ASSERT_NOT_COMPILES(C(MK_DEP(std::string{})));

    // assert fires: can add strings: OK!
    //STATIC_ASSERT_NOT_COMPILES(MK_DEP(s)+cs+std::string());

    // Too many arguments to MK_DEP is forced to give hard error: Fails to compile, OK!
    // STATIC_ASSERT_NOT_COMPILES(MK_DEP(1,2,3)+1);

    // Forgetting to add MK_DEP also gives a *hard* error. Fails to compile. OK!
    // STATIC_ASSERT_NOT_COMPILES(7=c);
}

Implementation, relying on Yakk's test_invoke and RETURNS. Feedback welcome!

namespace details{    
    struct make_depend{
        template<typename T> static constexpr const bool false_t = false;
        template<typename T>
        auto operator()(T&& arg) RETURNS(arg) ;
        // Try to protect against wrong use: zero or many arguments:
        template<typename T, typename... T2>
        auto operator()(T&& arg, T2... too_many_arguments) { 
            static_assert(false_t<T>, "Too many arguments given to MK_DEP"); } ;
        template<typename T=int>
        auto operator()()  { static_assert(false_t<T>, "Too few arguments given to MK_DEP"); } ;
    };
}

#define STATIC_ASSERT_NOT_COMPILES(...)\
    static_assert(!test_invoke([&](auto MK_DEP)RETURNS(__VA_ARGS__))\
       (details::make_depend{}))

Alternatively, a somewhat less wrapped approach:

#define CHECK_COMPILES(...)\
    test_invoke([&](auto MK_DEP)RETURNS(__VA_ARGS__))(details::make_depend{})

static_assert(CHECK_COMPILES(cs=MK_DEP(s)));

Or even just the basic idea:

static_assert(test_invoke([&](auto MK_DEP)RETURNS(s+MK_DEP(s)))(details::make_depend{}));

Compiler explorer demo

EDIT: The variadic operator() is just to protect against some cases of wrong use of MK_DEP. I also added a no-argument version for the same reason.

like image 4
Johan Lundberg Avatar answered Nov 15 '22 23:11

Johan Lundberg