I would like to make a class method have a signature that matches the class template parameter, e.g., for a Class<T>
, there's Class<T>::Method(T t)
if T
is not void
, and Class<T>::Method()
if T
is void
.
Compiling the attempted code below, if T
is void
, then both methods exist. I'm not understanding why the same use of enable_if
works for foo
but not that version of print
.
#include <iostream>
using namespace std;
template <typename T>
struct S
{
template <typename U = T>
std::enable_if_t<std::is_void_v<U>, void>
print(void) { cout << "print void" << endl; }
template <typename U = T>
std::enable_if_t<not(std::is_void_v<U>), void>
print(U x) { cout << "print not void " << x << endl; }
template <typename U = T>
std::enable_if_t<not(std::is_void_v<U>), void>
foo() {};
// ... rest of class
};
int main()
{
S<int> i;
i.print(1);
// i.print(); // is disabled
S<void> v;
v.print();
v.print(2); // should be disabled but is not
v.foo(); // same enable_if use disabled this method
}
v.foo<int>()
you are incorrect in thinking that the version of foo
on the void version of S
doesn't exist. The above code will run perfectly fine.
What you have shown is that the default template parameters pick a version that doesn't exist.
In the case of print
, you provide default template parameters - but you also allow them to be both passed (ie, .print<double>(1)
) explicitly, or be deduced (as in .print(2)
).
In both of those cases, this result in a U
type that is not T
, hence not void
.
There are at least 2 approaches. You can block deduction (getting the foo
style not-exist to work for print
) and you can even block passing explicit parameters (preventing foo<int>()
from being called). Second, you can also use requires
, which is cleaner but requires a newer version of C++.
To block deduction, you can use std::identity_t
or you own hand-rolled equivalent:
template <typename U = T>
std::enable_if_t<std::is_void_v<U>, void>
print() { std::cout << "print void" << std::endl; }
template <typename U = T>
std::enable_if_t<not(std::is_void_v<U>), void>
print(std::type_identity_t<U> x) { std::cout << "print not void " << x << std::endl; }
to block passing parameters explicitly, do this:
template <class..., class U = T>
std::enable_if_t<std::is_void_v<U>, void>
print() { std::cout << "print void" << std::endl; }
now any passed parameters get "swallowed up" by the class...
and thrown away.
Doing both gives you a older-standard implementation that does most of what you want.
We can do better with requires
however:
template <typename T>
struct S
{
void print() requires (std::is_same_v<T,void>)
{ std::cout << "print void" << std::endl; }
template<class..., class U=T>
void print(std::type_identity_t<U> x) requires (!std::is_same_v<T,void>)
{ std::cout << "print not void " << x << std::endl; }
void foo() requires (!std::is_same_v<T, void>)
{};
};
which sadly still requires some similar machinery for print
.
As an alternative:
void print(std::convertible_to<T> auto x) requires (!std::is_same_v<T,void>)
{ std::cout << "print not void " << static_cast<T>(x) << std::endl; }
using concepts to constrain the argument to be T
compatible.