2

I've runned into some trouble trying to make friend declarations with sfinae checks (you can just jump into the code sample if you don't want explanations on "why" and "how").

Basically, I have some template class declaring two private member functions. Depending on the instantiation of the template type, I want to use either one or the other function.

So, if I don't want compilation to fail, the private function that I cannot use cannot be instantiated. So, I must called it through an sfinae check (an independent function). Considering it is private, I have to make my sfinae check a friend of my class.

However, I'm unable to do that, as the following (minimal) code illustrates. The things I don't want to change : the prototype of class A (f1 and f2 must remain private), the prototypes of class B1 and B2.

I understand why the stuff in comments fails (or I think I do), but I don't know how to fix it.

#include <iostream>

template<class T> class A;

template<class T>
auto sfinae_check(T& t, A<T>& a, int)  -> decltype(t.b1(), void());

template<class T>
auto sfinae_check(T& t, A<T>& a, long)  -> decltype(t.b2(), void());

template<class T>
class A
{
    void f1() { t.b1(); }
    void f2() { t.b2(); }

    T& t;

    //friend auto sfinae_check<>(T &t, A<T> &a, int);//obviously mismatches everything
    //friend auto sfinae_check<>(T &t, A<T> &a, int) -> decltype(t.b1(), void()); //failure : no member named b1
    //friend auto sfinae_check<>(T &t, A<T> &a, long) -> decltype(t.b2(), void()); //failure : no member named b2

    public:
        A(T& t) : t(t) {}
        void f() { sfinae_check(t, *this, 0); }
};

template<class T>
auto sfinae_check(T& t, A<T>& a, int)  -> decltype(t.b1(), void())
{
    a.f1();
}

template<class T>
auto sfinae_check(T& t, A<T>& a, long)  -> decltype(t.b2(), void())
{
    a.f2();
}

struct B1
{
    void b1() { std::cout << "b1" << std::endl; }
};

struct B2
{
    void b2() { std::cout << "b2" << std::endl; }
};

int main()
{
    B1 b1; B2 b2;

    A<B1> a1(b1);
    a1.f(); //should print b1

    A<B2> a2(b2);
    a2.f(); //should print b2
}
Mike Kinghan
  • 55,740
  • 12
  • 153
  • 182
R. Absil
  • 173
  • 6

2 Answers2

3

This entire scheme can be simplified a great deal (in coupling too) if you forgo the different names f1 and f2 and do tag dispatch instead:

template<int> struct tag{};

template<int i> struct priority : priority<i - 1> {};
template<> struct priority <0>{};

template<class T>
auto sfinae_check(T& t, priority<1>)  -> decltype(t.b1(), tag<1>{}) { return {}; }

template<class T>
auto sfinae_check(T& t, priority<0>)  -> decltype(t.b2(), tag<0>{}) { return {}; }

template<class T>
class A
{
    void f(tag<1>) { t.b1(); }
    void f(tag<0>) { t.b2(); }

    T& t;

    public:
        A(T& t) : t(t) {}
        void f() { f(sfinae_check(t, priority<1>{})); }
};

No friendship, no almost circular dependencies, and you see the exact output you want. And as icing on the top, adding support for another overload should be fairly easy if the need arises.

The priority of the overloads is also encoded here (thank Jarod42 for reminding me). Since that tags are in an inheritance chain, the second argument priority<1>{} can be provided to either overloads, but in case both are viable, it will favor the closer match.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • Ok, I don't understand nearly anything. Do you have some reference I would read in order to understand what it is about ? – R. Absil Oct 11 '18 at 09:29
  • @R.Absil - "tag dispatch" is a term worth looking up. The idea is to just use a small template to encode type information we leverage in overload resolution. The key is being "small". Passing those tags by value is cheap. – StoryTeller - Unslander Monica Oct 11 '18 at 09:33
  • Thanks, I'll have a carefull look about it. Your solution works like a charm (and has the advantages you point out). – R. Absil Oct 11 '18 at 09:38
  • @R.Absil - Glad I could be of help. [Here's an online demo](http://coliru.stacked-crooked.com/a/fcc6e9e006166807). I checked it by adding support for another member and another type. I also mixed up the members a bit more between classes. You can play with it and see how the prioritization works. – StoryTeller - Unslander Monica Oct 11 '18 at 09:40
  • By any chance, do you knoow how I could make it work with a syntax similar to what I did ? Something like @Frank did, but without the slight drawback I explained in the comments ? – R. Absil Oct 12 '18 at 12:49
  • @R.Absil - I do not think you can. The issue is that you have two overloaded template functions. You want to befriend a specific instantiation of both, but that one may not exist (it's ill formed on substitution). Overload resolution silently ignores the ill formed one, (that's SFINAE), but other contexts (like friend declarations) must report it's ill-formed. There will be a simpler way to constrain functions in C++20, that may in fact give you exactly what you want, but there is no way today. – StoryTeller - Unslander Monica Oct 12 '18 at 12:57
  • Thanks. Btw, your edit with priorities and inheritance is really interesting. I think I'll use that from now on :) – R. Absil Oct 15 '18 at 12:05
2

A general simple solution to dealing with templated friend functions in templated classes is to just declare the template itself as friend instead of just the overload specific to the current instantiation:

template<class T>
class A
{
    ...

    template<typename U>
    friend auto sfinae_check(U &u, A<U> &a, int) -> decltype(u.b1(), void()); 

    template<typename U>
    friend auto sfinae_check(U &u, A<U> &a, long) -> decltype(u.b2(), void()); 

    ...
};

Which seems to solve your issue: https://gcc.godbolt.org/z/8UGGZM

  • I really like the simplicity of this solution, but it doesn't do quite the same : in your code, every instanciation of sfinae_check is a friend of A, and not only instanciations of sfinae_check (same template parameter) – R. Absil Oct 11 '18 at 09:20
  • @R.Absil Personally, I don't see this as that big of a drawback. A friend function is already assumed to only touch the parts of the class that are relevant to what it needs to do. In that sense, it already has way more permissions than it actually needs. I see this as friending in the code stored in the template, so it's not that big of a stretch to assume it will only mess with private members of the type it's currently overloading on. –  Oct 12 '18 at 14:56