airis
airis

Reputation: 171

SFINAE dispatch between void and non-void method

I have something like the following:

template <typename T>
struct Base {
    auto func() {
        // do stuff
        auto x = static_cast<T&>(*this).func_impl();
        // do stuff
        return x;
    }
};

struct A : Base<A> {
    int func_impl() {
        return 0;
    }
};

struct B : Base<B> {
    void func_impl() {
    }
};

int main() {
    A a;
    int i = a.func();
    B b;
    b.func();
    return 0;
}

The problem is that i cannot declare the return type of func_impl in a derived class to void as shown in B. I tried to solve the problem using SFINAE like this:

template <typename T>
struct Base {
    template <typename = enable_if_t<!is_void<decltype(declval<T>().func_impl())>::value>>
    auto func() {
        // do stuff
        auto x = static_cast<T&>(*this).func_impl();
        // do stuff
        return x;
    }

    template <typename = enable_if_t<is_void<decltype(declval<T>().func_impl())>::value>>
    void func() {
        // do stuff
        static_cast<T&>(*this).func_impl();
        // do stuff
    }
};

But the compiler gives the error: invalid use of incomplete type 'struct A' and invalid use of incomplete type 'struct B'. Is there a way to accomplish what I want?

Upvotes: 2

Views: 386

Answers (2)

Barry
Barry

Reputation: 303957

Situations like this:

auto x = static_cast<T&>(*this).func_impl();
// do stuff
return x;

call for a Regular Void type. In other words, since you don't need x in here, you just need to return it, do you really need func() to return void? I find that's typically not the case. Any old empty type that people aren't supposed to use is good enough. So let's make it easy to write this case without duplication:

namespace details {
    struct Void { }; // not intended to be used anywhere
}

template <typename F, typename... Args,
    typename R = std::invoke_result_t<F, Args...>,
    std::enable_if_t<!std::is_void<R>::value, int> = 0>
R invoke_void(F&& f, Args&&... args) {
    return std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
}

template <typename F, typename... Args,
    typename R = std::invoke_result_t<F, Args...>,
    std::enable_if_t<std::is_void<R>::value, int> = 0>
details::Void invoke_void(F&& f, Args&&... args) {
    std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
    return details::Void{};
}

This implementation uses C++17 library features, but can be implemented in C++14. This gives us an invoke() that swaps out void for Void, which allows you to just write:

auto func() {
    // do stuff
    auto x = invoke_void([](auto& x){ return x.func_impl(); },
        static_cast<T&>(*this));
    // do stuff
    return x;
}

It's a little wordy, but at least we don't have to duplicate func() - just the one function handles both cases just fine.


A different alternative, that's either simpler or more complex depending on your interpretation, is to re-order the body of func():

auto func() {
    // do stuff
    scope_exit{
        // do stuff after func_impl is invoked
    };
    return static_cast<T&>(*this).func_impl();
}

This gets you the correct ordering of operations without even needing a regular void. However, the post-func_impl logic gets placed before it - which may be confusing. But the benefit is that this function can still return void.

There are numerous implementations of a thing like scope_exit on SO.

Upvotes: 1

max66
max66

Reputation: 66230

Try with

template <typename T>
struct Base {
    template <typename U = T, typename = enable_if_t<!is_void<decltype(declval<U>().func_impl())>::value>>
    auto func() {
        // do stuff
        return static_cast<T&>(*this).func_impl();
    }

    template <typename U = T, typename = enable_if_t<is_void<decltype(declval<U>().func_impl())>::value>>
    void func() {
        // do stuff
        static_cast<T&>(*this).func_impl();
    }
};

I mean... SFINAE is applied over templates; if you want enable/disable methods inside a class, they have to be template methods (the fact that the class/struct is a template class/struct doesn't count: is the method that have to be template.

And the SFINAE part (std::enable_if_t, in this case) have to depend from a template of the method (U, in my example).

P.s: anyway, I don't see problems returning void

Upvotes: 5

Related Questions