Reputation: 171
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
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
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