3

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.

3 Answers3

6

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.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • Any difference between using `return (body);` and `return body;`? I guess the latter allows some nonsense/trickery on the macro user side, but does it affect any of the involved types in the "sane" use case as described? – Max Langhof Sep 17 '19 at 14:19
  • 1
    @MaxLanghof Yes, if body is an identifier... `return x;` and `return (x);` would mean different things in this context. But my understanding of OP's use-case is that body is really going to look a function call, so the parens aren't necessary. – Barry Sep 17 '19 at 14:36
1

You can do this without a macro (which I recommend):

template <class F>
constexpr auto troy(F f)
{
    using R = decltype(f());
    static_assert(!std::is_reference_v<R>);

    if constexpr (std::is_void_v<R>)
    {
        try
        {
            f();  
        }
        catch(...)
        {
        }
        return std::optional<bool>{std::nullopt};
    }
    else
    {
        try
        {
            return std::optional<R>(f());
        }
        catch(...)
        {
            return std::optional<R>{std::nullopt};
        }

    }
}
auto foo1() {}
auto foo2() { return 24; }

auto test()
{
    auto a = troy(foo1);
    auto b = troy(foo2);
}

And if you have an overloaded function there is a simple fix:

auto foo(int a) -> void;
auto foo(int a, int b) -> void;


auto test()
{
    // your version:
    // auto f = troy(foo(1024, 11));

    // my version:
    auto f = troy([] { return foo(1024, 11); });
}
bolov
  • 72,283
  • 15
  • 145
  • 224
  • The subtle difference here is that `troy` should "take" the full function invocation, not a function pointer/functor or similar. In other words, the original works even with an overloaded functor invocation and arbitrary parameter lists, not just parameter-less functions. Now, that is somewhat easy to restore by wrapping the input in a lambda (as the other answers do), but that will still require a macro. – Max Langhof Sep 17 '19 at 14:13
  • @MaxLanghof ok, I see. Good point. However in this particular case, because you know exactly what the invocation looks like you don't need the overload macro. See edit. – bolov Sep 17 '19 at 14:23
1

The compiler will do what it can to find errors in the false branch of a if.

Since there is no dependent types (it's not a template) then the compiler can check every branch for errors.

So invalid use of void expressions, using non declared symbols and other things like this are not valid in any branch, constexpr or not.

Dependent expression are different. The program is ill formed only if a branch is invalid if no type will make the branch compile. So use of undeclared symbols still cannot be used but there will surely be a type that will make std::optional<inner_type>{(body)} valid, so the compiler will simply not instantiate that branch.

That can be implemented with a function template then your macro calling it with a lambda:

template<typename T>
auto try_impl(T closure) noexcept {
    using body_type = decltype(closure());
    constexpr auto inner_type_is_void = std::is_void_v<body_type>;
    using inner_type = std::conditional_t<inner_type_is_void, bool, body_type>;

    try {
        if constexpr (std::is_void_v<body_type>) {
            closure();
            return std::optional<inner_type>{true};
        } else {
            return std::optional<inner_type>{closure()};
        }
    } catch(...) {
        return std::optional<inner_type>{};
    }
}

#ifndef troy
#define troy(body) try_impl([&]{ return body; })
#endif

Live example

Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141