Home > Back-end >  C Detect private member of friend class with CRTP
C Detect private member of friend class with CRTP

Time:04-09

I have a CRTP Base class (Bar) which is inherited by a unspecified class. This Derived class may or may not have specific member (internal_foo), and this specific member my or may not have another member (test()).

In this scenario internal_foo will always be public, however test() is private, but declaring Bar as a friend.

I can detect internal_foo using traits fine, because it is public. But I cannot detect test() due to it being private, even though Bar is a friend.

The below example works due to test() being public:

template<class, class = void >
struct has_internal_foo : std::false_type {};

template<class T>
struct has_internal_foo<T,
    void_t<
    decltype(std::declval<T>().internal_foo)
    >> : std::true_type {};

template<class, class = void>
struct internal_foo_has_test : std::false_type {};

template<class T>
struct internal_foo_has_test<T,
    void_t<decltype(std::declval<T>().internal_foo.test())
    >> : std::true_type {};

class InternalFoo
{
public:

    void test()
    {

    }
};

class BadInternalFoo
{
};

template<class T>
class Bar
{
public:
    template<class _T = T>
    std::enable_if_t<conjunction<has_internal_foo<_T>, internal_foo_has_test<_T>>::value, void>
        action()
    {
        static_cast<T&>(*this).internal_foo.test();
    }
};

class Foo : 
    public Bar<Foo>
{
public:
    InternalFoo internal_foo;
};

class BadFoo :
    public Bar<BadFoo>
{
public:
    BadInternalFoo internal_foo;
};

void test()
{
    Foo foo;
    BadFoo bad_foo;

    foo.action(); // Compiles. As expected.
    bad_foo.action(); // Does not compile. As expected.
}

However this next version does not work, due to test() being private:

template<class, class = void >
struct has_internal_foo : std::false_type {};

template<class T>
struct has_internal_foo<T,
    void_t<
    decltype(std::declval<T>().internal_foo)
    >> : std::true_type {};

template<class, class = void>
struct internal_foo_has_test : std::false_type {};

template<class T>
struct internal_foo_has_test<T,
    void_t<decltype(std::declval<T>().internal_foo.test())
    >> : std::true_type {};

class InternalFoo
{
public:
    template<class T>
    friend class Bar;

    template<class, class>
    friend struct internal_foo_has_test;

private:
    void test()
    {

    }
};

class BadInternalFoo
{
};

template<class T>
class Bar
{
public:
    template<class _T = T>
    std::enable_if_t<conjunction<has_internal_foo<_T>, internal_foo_has_test<_T>>::value, void>
        action()
    {
        static_cast<T&>(*this).internal_foo.test();
    }
};

class Foo : 
    public Bar<Foo>
{
public:
    InternalFoo internal_foo;
};

class BadFoo :
    public Bar<BadFoo>
{
public:
    BadInternalFoo internal_foo;
};

void test()
{
    Foo foo;
    BadFoo bad_foo;

    foo.action(); // Does not compile
    bad_foo.action(); // Does not compile
}

As seen above, I have tried to friend the detection struct too, but that didn't help.

Is there a way to do what I am trying to do?

Ideally I would like this solution to be portable, and not use anything beyond C 11, 14 at the most. (I have implemented void_t & conjunction)

Edit:

The suggested question does not answer this one. That question wants to detect whether a member is public or private, and only access it if it is public, I wish for the detection to return positive on a private member of a friend class.

CodePudding user response:

Summary and the fix

Looks like a GCC 11 bug and your second attempt should in fact work.

However, I recommend rewriting action's definition in either of two ways so you don't even need the member detection idiom:

// Way 1
template<class _T = T>
decltype(std::declval<_T&>().internal_foo.test()) action() {
    static_cast<T&>(*this).internal_foo.test();
}

// Way 1, with a different return type via the comma operator
template<class _T = T>
decltype(std::declval<_T&>().internal_foo.test(), std::declval<ReturnType>()) action() {
    static_cast<T&>(*this).internal_foo.test();
}

// Way 2
template<class _T = T>
auto action() -> decltype(static_cast<_T&>(*this).internal_foo.test()) {
    static_cast<_T&>(*this).internal_foo.test();  // Using _T for consistency
}

Note that I use _T inside the decltype so it's dependent on the template argument and can be SFINAEd. Also note that it's still possible to specify an arbitrary return return type without any enable_ifs.

Details

I took the liberty to prepend #include <type_traits> and using namespace std; to both of your examples and using C 17 so they can be compiled.

Some findings from the comments section:

  1. Your first code (does not) compile(s) as expected with Clang 14, gcc 11 and gcc trunk: https://godbolt.org/z/EbaYvfPE3
  2. Your second code (does not) compile(s) as expected with Clang add gcc trunk, but gcc 11 differs: https://godbolt.org/z/bbKrP8Mb9

There is an easier reproduction example: https://godbolt.org/z/T17dG3Mx1

#include <type_traits>

template<class, class = void>
struct has_test : std::false_type {};

template<class T>
struct has_test<T, std::void_t<decltype(std::declval<T>().test())>> : std::true_type {};

class HasPrivateTest
{
public:
    template<class, class>
    friend struct has_test;
    friend void foo();

private:
    void test() {}
};

// Comment the following line to make it compile with GCC 11
static_assert(has_test<HasPrivateTest>::value, "");
void foo() {
    static_assert(has_test<HasPrivateTest>::value, "");
}
static_assert(has_test<HasPrivateTest>::value, "");

The code above compiles with Clang 14 and gcc trunk, but is rejected by gcc 11 with three "static assertion failed" messages, one for each assertion. However, commenting out the first static_assert makes all three compilers accept the code.

So it seems like GCC 11 (and earlier) tries to instantiate the template and do access checks depending on the context. Hence, if the first instantiation is outside of a friend, .test() method is not accessible, and the result is kind cached. However, if it's inside the void foo() friend, .test()is accessible and allstatic_assert`s succeed.

@Klaus have pointed out a recent GCC bug whose fix seems relevant: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=96204

  • Related