隐藏友元概念在C++中

huangapple go评论81阅读模式
英文:

Hidden Friend Concept in C++

问题

我仍然是C++的初学者,试图更多地了解这门语言。我最近阅读了ADL(Argument-Dependent Lookup)和Hidden Friends idiom(https://www.modernescpp.com/index.php/argument-dependent-lookup-and-hidden-friends)的概念。我对ADL的理解是,在未经限定的函数调用情况下,C++会在当前命名空间和参数类型的命名空间中查找函数。

我对隐藏友元习惯的目的感到困惑,以及隐藏友元究竟意味着什么(即它有何隐藏之处)。我知道类的友元函数是非成员函数,但可以访问类的私有成员。然而,我不明白它们为什么是必需的。在阅读中提供的代码示例中,它特别指出了在自定义类的两个参数的一般重载中,给定函数的友元的必要性。也就是说,在

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;

};

类的+运算符重载不是友元,而是成员函数,从技术上讲,在这里它接受了3个参数MyDistance,因为它是成员函数(this)并且接受了2个额外的参数,使其无效。

然而,不使用隐藏友元,我们是否可以将代码编写为

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

这样编写代码有什么不利之处吗?它是否因为C++查找的顺序(可能会在查找成员函数之前查找非成员函数)而导致编译时速度较慢?另外,“隐藏友元习惯”到底是要“隐藏”什么?是不是函数本身是在类内部而不是外部定义的?

英文:

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?

答案1

得分: 4

有没有什么不利之处?是的,在你上面的示例中,C++对operator+的两个参数应用不同的规则。具体来说,左侧参数必须是类型为MyDistance的对象,但右侧参数可以是任何可转换为MyDistance的类型。

稍微扩展你的示例:

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

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

使用这段代码,下面的语句是合法的:

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

因为存在从intMyDistance的转换,但下面的语句是不合法的:

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

因为根据上面的声明,+的左侧必须是一个MyDistance对象。

operator+是友元函数时,不存在这样的问题,因为在这种情况下,任一参数都可以转换为MyDistance,上面两个版本的代码都是合法的。

我们对operator+的期望是它是对称的,所以友元版本更好,因为它对两个参数应用相同的规则。

英文:

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.

答案2

得分: 2

隐藏友元是你的朋友

Dan Saks 在CppCon2018上做了一个很棒的演讲,解释了隐藏友元。它的标题是结交新朋友

除了由@john解释的问题之外,模板是掌握“隐藏友元”惯用法的另一个重要原因。

流插入和提取运算符,operator<<operator>>,最好是基于std::basic_ostreamstd::basic_istream来编写的,这是std::ostreamstd::istream所基于的模板。以这种方式编写的运算符将与任何字符类型一起工作。

当你要读写的对象本身是模板时,事情可能会变得非常复杂。如果流插入和提取运算符函数不隐藏在对象类内部,而是在外部编写,那么你必须同时为对象和流使用模板参数。当运算符函数被编写为隐藏友元时,在对象类内部,你仍然需要为流提供模板参数,但不需要为对象提供模板参数。

例如,假设你决定为MyDistance类添加一个模板参数。如果operator<<不是隐藏友元,代码可能如下所示。这个operator<<位于MyDistance类外部的作用域中,可以在不使用ADL的情况下找到。

这是一个完整的程序(它可以运行):

#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<<的定义,并且该运算符函数模板是我的朋友。”
    //
    // 尽管它是一个友元,但它不是隐藏的。
    //
    // operator<<需要三个模板参数。
    // 参数NumType2与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<<不是隐藏的,因为它在MyDistance类外部定义,因此在MyDistance类外部的作用域中是可见的。可以在不使用ADL的情况下找到它。
//
// 在这里,我们可以使用NumType、NumType2、T或任何其他名称作为第三个模板参数。它只是一个名称。
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;
}

当作为隐藏友元编写时,代码既更清晰又更简洁。这个operator<<在MyDistance类外部的作用域中不可见,只能在使用ADL时找到。

这也是一个完整的程序:

#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<<只有std::basic_ostream需要的两个模板参数。它只在MyDistance类内部可见,因此它是“隐藏的”。
    //
    // 你也不能使用作用域解析运算符(::)来引用它,因为它不是类的成员!
    // 
    // 它真正是隐藏的,只能在使用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;
}

现在,想象一下MyDistance是一个更复杂的对象,具有许多模板参数,其中一些可能是模板化的。

几年前,我构建了一个名为RomanNumeral<IntType>的类,用于进行罗马数字的算术运算。我还编写了一个名为Rational<IntType>的类,用于进行有理数的算术运算,其中分子和分母分别存储。然后,我想到了一个聪明的主意,允许使用罗马数字构建有理数!但我还希望Rational类继续与整数一起工作。多么混乱的情况!需要非常小心地编写流运算符,以便它们输出像xiii/c这样的内容。

这是一个很好的练习。如果你尝试一下,你会发现隐藏友元是你的朋友!

英文:

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&lt;&lt; and operator&gt;&gt; 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&lt;&lt; is not a hidden friend, the code might look like the following. This operator&lt;&lt; resides in the scope outside of class MyDistance, and can be found without ADL.

This is a complete program (it runs):

#include &lt;iostream&gt;
#include &lt;type_traits&gt;

template&lt; typename NumType &gt;
class MyDistance {
    static_assert(std::is_arithmetic_v&lt;NumType&gt;, &quot;&quot;);
public:
    explicit MyDistance(NumType i) :m(i) {}

    // ...

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

private:
    NumType m;
};

// operator&lt;&lt; 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&#39;s just a name.
template&lt; typename charT, typename traits, typename NumType &gt;
auto operator&lt;&lt; (
    std::basic_ostream&lt;charT, traits&gt;&amp; out,
    const MyDistance&lt;NumType&gt;&amp; myDist
    )
    -&gt; std::basic_ostream&lt;charT, traits&gt;&amp;
{
    out &lt;&lt; myDist.m &lt;&lt; &quot; m&quot;;
    return out;
}

int main()
{
    MyDistance&lt;int&gt; md_int{ 42 };
    MyDistance&lt;double&gt; md_double{ 3.14 };
    std::cout 
        &lt;&lt; &quot;MyDistance&lt;int&gt;    : &quot; &lt;&lt; md_int &lt;&lt; &#39;\n&#39; 
        &lt;&lt; &quot;MyDistance&lt;double&gt; : &quot; &lt;&lt; md_double &lt;&lt; &#39;\n&#39;;
    return 0;
}

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

This is also a complete program:

#include &lt;iostream&gt;
#include &lt;type_traits&gt;

template&lt; typename NumType &gt;
class MyDistance {
    static_assert(std::is_arithmetic_v&lt;NumType&gt;, &quot;&quot;);
public:
    explicit MyDistance(NumType i) :m(i) {}

    // ...

    // operator&lt;&lt; has only the two template parameters 
    // required by std::basic_ostream. It is only visible 
    // within class MyDistance, so it is &quot;hidden.&quot; 
    //
    // 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&lt; typename charT, typename traits&gt;
    friend auto operator&lt;&lt; (
        std::basic_ostream&lt;charT, traits&gt;&amp; out,
        const MyDistance&amp; myDist
        )
        -&gt; std::basic_ostream&lt;charT, traits&gt;&amp;
    {
        out &lt;&lt; myDist.m &lt;&lt; &quot; m&quot;;
        return out;
    }

private:
    NumType m;
};

int main()
{
    MyDistance&lt;int&gt; md_int{ 42 };
    MyDistance&lt;double&gt; md_double{ 3.14 };
    std::cout
        &lt;&lt; &quot;MyDistance&lt;int&gt;    : &quot; &lt;&lt; md_int &lt;&lt; &#39;\n&#39;
        &lt;&lt; &quot;MyDistance&lt;double&gt; : &quot; &lt;&lt; md_double &lt;&lt; &#39;\n&#39;;
    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&lt;IntType&gt; to do arithmetic with Roman numerals. I also wrote class Rational&lt;IntType&gt; 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!

huangapple
  • 本文由 发表于 2023年7月23日 13:34:13
  • 转载请务必保留本文链接:https://go.coder-hub.com/76746760.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定