I am using C++17. I have code that is like the following:
#include <type_traits>
template <typename T>
struct Fooer
{
Fooer (T & fooable)
{
fooable . foo ();
}
};
template <typename T>
Fooer (T & fooable) -> Fooer <T>;
struct Fooable
{
private:
void
foo ();
friend struct Fooer <Fooable>;
};
struct NotFooable
{
};
I want to implement a type trait that can tell if a type is 'Fooable'.
I can't check to see if there's a method foo ()
on the type, because it is a private method. This also doesn't tell me if Fooer
's constructor can call the method.
// Checking for the foo method doesn't work.
template <typename T, typename = void>
struct HasFoo;
template <typename T, typename>
struct HasFoo : std::false_type
{
};
template <typename T>
struct HasFoo
<
T,
std::enable_if_t
<
std::is_convertible_v <decltype (std::declval <T> () . foo ()), void>
>
>
: std::true_type
{
};
// Both of these assertions fail.
static_assert (HasFoo <Fooable>::value);
static_assert (HasFoo <NotFooable>::value);
I also can't check to see if Fooer <T>
is constructible via std::is_constructible
, because std::is_constructible
doesn't check to see if the constructor definition is well-formed, only the expression Fooer <T> fooer (std::declval <T> ())
.
// Checking constructibility doesn't work either.
template <typename T, typename = void>
struct CanMakeFooer;
template <typename T, typename>
struct CanMakeFooer : std::false_type
{
};
template <typename T>
struct CanMakeFooer
<
T,
std::enable_if_t <std::is_constructible_v <Fooer <T>, T &>>
>
: std::true_type
{
};
// Neither of these assertions fail.
static_assert (CanMakeFooer <Fooable>::value);
static_assert (CanMakeFooer <NotFooable>::value);
If I actually try to call the constructors, I get the error that I expect, though it does not get me closer to implementing a type trait.
void
createFooer ()
{
Fooable fooable;
NotFooable not_fooable;
// This works fine.
{ Fooer fooer (fooable); }
// This correctly generates the compiler error: no member named 'foo' in
// 'NotFooable'
{ Fooer fooer (not_fooable); }
}
I want to avoid declaring the type trait as a friend of the Fooable types, and I want to avoid making 'foo' public.
If I could somehow make a type trait check the definition of a function or constructor for well-formedness, I could implement this type trait easily enough, but I don't know how to do that, and I can't find any examples of such a thing on the internet.
Is it possible to do what I want? How do I do this?
You need to make the call to foo()
part of the declaration of the Fooer
constructor and make the constructor SFINAE friendly. You can do this with a constructor template and a default template argument for requirements. This means that HasFoo
only needs to check if a Fooer
can be constructed with T
and doesn't have to worry about the foo()
function.
template <typename T>
struct Fooer {
template <typename U, typename = std::void_t<
decltype(std::declval<U &>().foo()),
std::enable_if_t<std::is_same_v<T, U>>
>>
explicit Fooer(U &fooable) {
fooable.foo();
}
};
template <typename U>
Fooer(U &) -> Fooer<U>;
template <typename T>
struct HasFoo : std::bool_constant<
std::is_constructible_v<Fooer<T>, T &>
> {};
struct Fooable {
private:
void foo() {}
friend struct Fooer<Fooable>;
};
struct NotFooable {};
static_assert(HasFoo<Fooable>::value);
static_assert(!HasFoo<NotFooable>::value);
The trouble here is that the constructor of Fooer
is not "SFINAE-friendly". It has a requirement that Fooer
can call fooable.foo()
, but as far as C++ is concerned, the declaration Fooer(T &);
doesn't have any such constraint.
We can change the constructor declaration into a constructor template so that template argument deduction fails for it when the template argument of the class template is not "fooable":
#include <utility>
template <typename T>
struct Fooer
{
template <typename U = T, typename Enable =
std::void_t<decltype(std::declval<U&>().foo())>>
Fooer (T & fooable)
{
fooable . foo ();
}
};
[This will become easier and more legible with C++20 constraints:
// C++20 code
template <typename T>
struct Fooer
{
Fooer (T & fooable) requires requires { fooable.foo(); }
{
fooable . foo ();
}
};
]
With that change, your CanMakeFooer
should work. Though it could be defined more simply with just the primary template and no specializations:
template <typename T>
struct CanMakeFooer :
public std::bool_constant<std::is_constructible_v<Fooer<T>, T&>>
{};
Demo on coliru.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With