Hidden Friend Concept in C++

480 views Asked by At

I'm still a beginner in C++ trying to learn more about the language. I recently read about the concept of ADL (Argument-Dependent Lookup) and Hidden Friends idiom (https://www.modernescpp.com/index.php/argument-dependent-lookup-and-hidden-friends). My understanding of ADL is that in the case of an unqualified function call, C++ looks for the function in not only the current namespace, but also the namespace of the argument type.

I'm confused at what the point of the hidden friend idiom is, and what hidden friend means exactly (i.e. what is hidden about it). I get that friend functions of a class are non-member functions but can access private members of the class. However, I don't see why they are necessary. In the code example given in the reading, it points out the necessity of friends in the given functions specifically for general overloads with two parameters of a custom class. That is, in

class MyDistance{
  public:
    explicit MyDistance(double i):m(i){}

    MyDistance operator +(const MyDistance& a, const MyDistance& b){
        return MyDistance(a.m + b.m);
    }
    
    friend MyDistance operator -(const MyDistance& a, const MyDistance& b){
        return MyDistance(a.m - b.m);
    }
    
    friend std::ostream& operator<< (std::ostream &out, const MyDistance& myDist){
        out << myDist.m << " m";
        return out;
    }

  private:
    double m;

};

The + operator overload for the class is not a friend, is a member function, and technically takes in 3 parameters of MyDistance here I believe since it is a member function (this) and takes 2 additional parameters, making it invalid.

However, instead of having a hidden friend, couldn't we just write the code as

class MyDistance{
  public:
    ...
    
    MyDistance operator +(const MyDistance& other){
        return MyDistance(m + other.m);
    }
    ...
};

Is there any downside to writing the code like this? Is it slower (at compile time) in some way due to the order in which C++ does the lookup (perhaps looking at non-member functions before looking at member functions)? Also, what exactly is the "hidden friend idiom" supposed to "hide"? Is it that the function itself is defined in the class instead of outside?

2

There are 2 answers

6
john On BEST ANSWER

Is there any downside? Yes, in your example above C++ applies different rules to the two arguments of operator+. Specifically the left hand argument must be an object of type MyDistance but the right hand argument can be any type convertible to MyDistance.

Extending your example a little

class MyDistance{
  public:
    ...
    MyDistance(int dist) { ... }

    MyDistance operator+(const MyDistance& other) const {
        return MyDistance(m + other.m);
    }
    ...
};

With this code

MyDistance x(1);
MyDistance y = x + 2;

is legal because there is a conversion from int to MyDistance but this is illegal

MyDistance x(1);
MyDistance y = 2 + x;

because given the declaration above the left hand side of + must be a MyDistance object.

There is no such problem when operator+ is a friend, in that case either argument can be convertible to MyDistance and both versions of the code above are legal.

Our expectation of operator+ is that it is symmetric, so the friend version is better because it applies the same rules to both arguments.

0
tbxfreeware On

Hidden Friends Are Your Friend

Dan Saks gave a great talk explaining hidden friends at CppCon2018. It is entitled Making New Friends.

In addition to the issues explained by @john, templates are another big reason for mastering the "hidden friends" idiom.

The stream insertion and extraction operators, operator<< and operator>> are best written in terms of std::basic_ostream and std::basic_istream, the templates on which std::ostream and std::istreamare based. Written that way, the operators will work with any character type.

When the objects you are reading and writing are themselves templates, things can get complicated fast. If the stream insertion and extraction operator functions are not hidden inside the object class, and are instead written outside of it, you have to use template paramaters both for the object and the stream. When the operator functions are written as hidden friends, inside of the object class, you still need to supply template parameters, but only for the stream (and not for the object).

Suppose, for instance, you decide to add a template parameter to class MyDistance. If operator<< is not a hidden friend, the code might look like the following. This operator<< resides in the scope outside of class MyDistance, and can be found without ADL.

This is a complete program (it runs):

#include <iostream>
#include <type_traits>

template< typename NumType >
class MyDistance {
    static_assert(std::is_arithmetic_v<NumType>, "");
public:
    explicit MyDistance(NumType i) :m(i) {}

    // ...

    // This is a declaration that says, in essence, "In the 
    // scope outside this class, there is visible a definition 
    // for the templated operator<< declared here, and that 
    // operator function template is my friend." 
    // 
    // Although it is a friend, it is not hidden.
    //
    // operator<< requires three template parameters.
    // Parameter NumType2 is distinct from NumType.
    template< typename charT, typename traits, typename NumType2 >
    friend auto operator<< (
        std::basic_ostream<charT, traits>& out,
        const MyDistance<NumType2>& myDist
        )
        -> std::basic_ostream<charT, traits>&;

private:
    NumType m;
};

// operator<< is not hidden, because it is defined outside
// of class MyDistance, and it is therefore visible in the 
// scope outside class MyDistance. It can be found without ADL.
//
// Here we can use NumType, NumType2, T, or anything else 
// as the third template parameter. It's just a name.
template< typename charT, typename traits, typename NumType >
auto operator<< (
    std::basic_ostream<charT, traits>& out,
    const MyDistance<NumType>& myDist
    )
    -> std::basic_ostream<charT, traits>&
{
    out << myDist.m << " m";
    return out;
}

int main()
{
    MyDistance<int> md_int{ 42 };
    MyDistance<double> md_double{ 3.14 };
    std::cout 
        << "MyDistance<int>    : " << md_int << '\n' 
        << "MyDistance<double> : " << md_double << '\n';
    return 0;
}

When written as a hidden friend, the code is both cleaner and more consise. This operator<< is not visible in the scope outside class MyDistance, and can only be found with ADL.

This is also a complete program:

#include <iostream>
#include <type_traits>

template< typename NumType >
class MyDistance {
    static_assert(std::is_arithmetic_v<NumType>, "");
public:
    explicit MyDistance(NumType i) :m(i) {}

    // ...

    // operator<< has only the two template parameters 
    // required by std::basic_ostream. It is only visible 
    // within class MyDistance, so it is "hidden." 
    //
    // You cannot scope to it either, using the scope resolution 
    // operator(::), because it is not a member of the class!
    // 
    // It is truly hidden, and can only be found with ADL.
    template< typename charT, typename traits>
    friend auto operator<< (
        std::basic_ostream<charT, traits>& out,
        const MyDistance& myDist
        )
        -> std::basic_ostream<charT, traits>&
    {
        out << myDist.m << " m";
        return out;
    }

private:
    NumType m;
};

int main()
{
    MyDistance<int> md_int{ 42 };
    MyDistance<double> md_double{ 3.14 };
    std::cout
        << "MyDistance<int>    : " << md_int << '\n'
        << "MyDistance<double> : " << md_double << '\n';
    return 0;
}

Now, imagine that MyDistance is a more complicated object, with many template parameters, some of which themselves might be templated.

A few years ago I constructed class RomanNumeral<IntType> to do arithmetic with Roman numerals. I also wrote class Rational<IntType> to do arithmetic with rational numbers, where numerator and denominator were stored separately. Then I got the bright idea of allowing rational numbers to be constructed with Roman numerals! But I also wanted class Rational to continue working with integers. What a mess! It took real care to get the stream operators working so they would output things like: xiii/c.

It's a great exercise. One of the things you will learn if you try it, is that hidden friends are your friend!