3

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.

uhsl_m
  • 322
  • 3
  • 11
  • 1
    If you just need conditional compilation on `action`, get rid of member detection and make `action` return `decltype(static_cast(*this).internal_foo.test())` instead of `enable_if_t`s – yeputons Apr 08 '22 at 11:06
  • 1
    Does this answer your question? [How to test access to a private function?](https://stackoverflow.com/questions/35775029/how-to-test-access-to-a-private-function) – Klaus Apr 08 '22 at 11:06
  • Aren't you using GCC by any chance? Does your code work with Clang? – yeputons Apr 08 '22 at 11:11
  • @yeputons: Thank you! I have even used that technique before, but I don't know why it didn't come to me this time. I am using Visual Studio 2019 right now, but I use GCC on Linux with the same code base, hence the portability. – uhsl_m Apr 08 '22 at 11:16
  • 2
    It seems that private members and friends [play weirdly with SFINAE](https://quuxplusone.github.io/blog/2019/09/10/friend-access-inconsistencies/). – yeputons Apr 08 '22 at 11:26
  • 2
    There was also a bug in gcc: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=96204. That is fixed in gcc 12 ( hopefully :-) ). – Klaus Apr 08 '22 at 11:33
  • Ahh yes, that issue with SFINAE under *Divergence on friend with templates* is pretty much exactly what I was butting up against. – uhsl_m Apr 08 '22 at 11:33
  • Here is a shorter example of different behavior by GCC and Clang: https://godbolt.org/z/T17dG3Mx1. Moreover, commenting out one of `static_assert`s in it makes GCC accept the code. Weird stuff. – yeputons Apr 08 '22 at 11:35
  • @yeputons: That is fixed also in gcc 12... maybe it is related to my previous command ( bugreport for gcc ). Simply use gcc 12 on godbolt, seams to work now! – Klaus Apr 08 '22 at 11:36
  • @yeputons that is definitely weird behaviour. – uhsl_m Apr 08 '22 at 11:38

1 Answers1

2

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 friend void foo(), .test() is accessible and all static_asserts succeed.

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

yeputons
  • 8,478
  • 34
  • 67
  • More or less with the same SFINAE stuff i was not successful which was the reason to fill the bug report. But I switched to concepts for such use cases... much easier to read I believe. – Klaus Apr 08 '22 at 11:57