Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to construct a type trait that can tell if one type's private methods can be called in another type's constructor?

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?

like image 494
Zistack Avatar asked Dec 05 '19 22:12

Zistack


2 Answers

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);
like image 86
Indiana Kernick Avatar answered Nov 10 '22 19:11

Indiana Kernick


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.

like image 44
aschepler Avatar answered Nov 10 '22 17:11

aschepler