Search code examples
c++c++17if-constexpr

How to make the compiler ignore this if-constexpr that evaluates to false?


I am writing a macro that when used to wrap a function call like so: macro(function()), returns a std::optional<T>, where T is the return value of the function. So far it works, but I run into problems when trying to get it to return a std::optional<bool> when the function has no return type (i.e. it returns void).

The core of the problem is that the compiler tries to evaluate the scope of a if-constexpr that evaluates to false when the function has no return type.

I tried using two if-constexprs and one with an else. I tried inlining everything and spreading it out a few lines. I tried replacing the compiler-calculated constexpr constants with constant literals. Nothing works so far.

The macro as I have it now looks like this:

#ifndef troy
#define troy(body)[](){\
    constexpr auto inner_type_is_void=std::is_same<decltype(body), void>::value;\
    using inner_type=std::conditional<inner_type_is_void,bool,decltype(body)>::type;\
    try{\
        if constexpr (inner_type_is_void) {(body); return std::optional<inner_type>{true};}\
        if constexpr (!inner_type_is_void) {return std::optional<inner_type>{(body)};}\
    } catch (...){\
        return std::optional<inner_type>{};\
    }\
}()
#endif

And I use it like so:

{
    auto a=troy(f());
    if (!a){
        std::cout << "fail" << std::endl;
    } else{
        std::cout << "success: " << *a << std::endl;
    }
}

It works when f returns a value, but gives a compiler error when not. This compiler error is: no matching function for call to ‘std::optional<bool>::optional(<brace-enclosed initializer list>)’ on the line where the if-constexpr evaluates to false.


Solution

  • In order to avoid doing instantiation for if constexpr, you need a context that actually has instantiation: you need some kind of template. Outside of a template, there's no magic power that if constexpr bestows.

    The solution, as always, is to just wrap everything in a lambda. Right now, you're using body in-line for the actual function body. Instead, you can use the lambda:

    [&]() -> decltype(auto) { return body; }
    

    And then wrap the whole thing in an immediately invoked lambda that takes this lambda as an argument:

    [](auto f){
        // stuff
    }([&]() -> decltype(auto) { return body; })
    

    Now, where I marked // stuff, we're in a template context and we can actually use if constexpr to avoid instantiations:

    [](auto f){
        using F = decltype(f);
        using R = std::invoke_result_t<F>;
        try {
            if constexpr (std::is_void_v<R>>) {
                f();
                return std::optional<bool>(true);
            } else {
                return std::make_optional(f());
            }
        } catch (...) {
            return std::optional<std::conditional_t<std::is_void_v<R>, bool, R>>();
        }
    }([&]() -> decltype(auto) { return body; })
    

    Now just add a bunch of \s and you're good to go.