Search code examples
c++templatesc++17type-traits

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?


Solution

  • 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.