友元声明

来自cppreference.com
< cpp‎ | language


 
 
C++ 语言
 
 

友元声明在类体内出现,并向一个函数或另一个类授予对包含友元声明的类的私有及受保护成员的访问权。

语法

friend 函数声明 (1)
friend 函数定义 (2)
friend 详述类型说明符 ; (3) (C++26 前)
friend 简单类型说明符 ;

friend typename说明符 ;

(4)
(C++26 前)(C++11 起)
friend 友元类型说明符列表 ; (5) (C++26 起)
1,2) 函数友元声明。
3-5) 类友元声明。
函数声明 - 函数声明
函数定义 - 函数定义
详述类型说明符 - 详述类型说明符
简单类型说明符 - 简单类型说明符
typename说明符 - 关键词 typename 后随有限定标识符或有限定简单模板标识
友元类型说明符列表 - 逗号分隔的包含简单类型说明符详述类型说明符 和typename说明符 的非空列表,每个说明符都可后随一个省略号(...

描述

1) 指明一个或多个函数作为此类的友元:
class Y
{
    int data; // 私有成员
 
    // 非成员函数的运算符 operator<< 将拥有对 Y 的私有成员的访问权
    friend std::ostream& operator<<(std::ostream& out, const Y& o);
    friend char* X::foo(int); // 其他类的成员也可以是友元
    friend X::X(char), X::~X(); // 构造函数与析构函数也可以是友元
};
 
// 友元声明不声明成员函数
// 此 operator<< 作为非成员仍需定义
std::ostream& operator<<(std::ostream& out, const Y& y)
{
    return out << y.data; // 可访问私有成员 Y::data
}
2) (只允许在非局部类的定义中使用)定义一个非成员函数,同时让它成为此类的友元。这种非成员函数始终是内联函数,除非它附着到一个具名模块 (C++20 起)
class X
{
    int a;
 
    friend void friend_set(X& p, int i)
    {
        p.a = i; // 这是非成员函数
    }
public:
    void member_set(int i)
    {
        a = i; // 这是成员函数
    }
};
3,4) 指定一个类作为此类的友元。这意味着该友元的各成员声明和定义可以访问此类的私有与受保护成员,而且该友元关系也能被此类的私有或受保护成员继承到其派生类。
3) 类以详述类说明符(见详述类型说明符)指名。不需要提前声明用于此友元声明的类名。
4) 类以简单类型说明符 或typename说明符 指名,如果指名的不是类类型,那么忽略该友元声明。此声明不会前置声明新类型。
5) 指定友元类型说明符列表 中的所有类作为此类的友元。这意味着这些友元的各成员声明和定义可以访问此类的私有与受保护成员,而且这些友元关系也能被此类的私有或受保护成员继承到其派生类。如果指名的某个类型不是类类型,那么在该友元声明中忽略它。
友元类型说明符列表 中的每个说明符在没有后随省略号的情况下指名一个类,否则适用包展开
class Y {};
 
class A
{
    int data; // 私有数据成员
 
    class B {}; // 私有嵌套类型
 
    enum { a = 100 }; // 私有枚举项
 
    friend class X; // 友元类的前置声明(详述类型说明符)
    friend Y; // 友元类声明(简单类型说明符)(C++11 起)
 
    // C++26 起以上两个友元声明可以合并:
    // friend class X, Y;
};
 
class X : A::B // OK:友元能访问 A::B
{
    A::B mx; // OK:友元的成员能访问 A::B
 
    class Y
    {
        A::B my; // OK:友元的嵌套成员能访问 A::B
    };
 
    int v[A::a]; // OK:友元的成员能访问 A::a
};

模板友元

函数模板类模板声明,都可带 friend 说明符出现于任何非局部类或类模板内(尽管只有函数模板能在授予友元关系的类或类模板内进行定义)。这种情况下,该模板的每个特化都成为友元,不管是被隐式实例化、部分特化或显式特化。

class A
{
    template<typename T>
    friend class B; // 每个 B<T> 都是 A 的友元
 
    template<typename T>
    friend void f(T) {} // 每个 f<T> 都是 A 的友元
};

友元声明不能指代部分特化,但能指代全特化:

template<class T>
class A {};      // 主模板
 
template<class T>
class A<T*> {};  // 部分特化
 
template<>
class A<int> {}; // 全特化
 
class X
{
    template<class T>
    friend class A<T*>;  // 错误!
 
    friend class A<int>; // OK
};

当友元声明指代函数模板的全特化时,不能使用关键词 inlineconstexpr (C++11 起)consteval (C++20 起) 和默认实参:

template<class T>
void f(int);
 
template<>
void f<int>(int);
 
class X
{
    friend void f<int>(int x = 1); // 错误:不允许默认实参
};

模板友元声明可以指名类模板 A 的成员,它可以是成员函数或成员类型(类型必须用详述类型说明符)。这种声明只有在它的 嵌套名说明符 中的最后组分(最后的 :: 左边的名字)是一个指名该类模板的 简单模板标识(模板名后随角括号包围的实参列表)时才是良构的。这种模板友元声明的模板形参必须能从该 简单模板标识 推导。

此时 A 的所有特化的成员,以及 A 的所有部分特化的成员都会成为友元。这不并涉及对主模板 A 和 A 的部分特化的实例化:只要求从该特化推导 A 的模板形参成功,以及将推导出的模板实参替换到友元声明中所产生的声明,是该特化的成员的合法声明:

// 主模板
template<class T>
struct A
{ 
    struct B {};
 
    void f();
 
    struct D { void g(); };
 
    T h();
 
    template<T U>
    T i();
};
 
// 全特化
template<>
struct A<int>
{
    struct B {};
 
    int f();
 
    struct D { void g(); };
 
    template<int U>
    int i();
};
 
// 另一全特化
template<>
struct A<float*>
{
    int *h();
};
 
// 非模板类向类模板 A 的成员授予友元关系
class X
{
    template<class T>
    friend struct A<T>::B; // 所有的 A<T>::B 都是友元,包括 A<int>::B
 
    template<class T>
    friend void A<T>::f(); // A<int>::f() 不是友元,因为其签名不匹配,
                           // 但比如 A<char>::f() 则是友元
 
//  template<class T>
//  friend void A<T>::D::g(); // 非良构:嵌套名说明符的最后部分,
//                            // A<T>::D:: 中 的 D,不是简单模板标识
 
    template<class T>
    friend int* A<T*>::h(); // 所有的 A<T*>::h 都是友元:A<float*>::h()、A<int*>::h() 等
 
    template<class T> 
    template<T U>       // A<T>::i() 的所有实例化和 A<int>::i() 都是友元,
    friend T A<T>::i(); // 从而这些函数模板的所有特化都是
};

只有在模板友元声明是定义,且此翻译单元不出现此函数模板的其他声明时,该声明中才允许使用默认模板实参

(C++11 起)

模板友元运算符

模板友元的一种常见使用场合是作用于类模板上的非成员运算符重载的声明,例如针对某个用户定义的 Foo<T>operator<<(std::ostream&, const Foo<T>&)

这种运算符可以在类体内定义,效果是对每个 T 生成独立的非模板 operator<<,并使该非模板 operator<< 作为它的 Foo<T> 的友元:

#include <iostream>
 
template<typename T>
class Foo
{
public:
    Foo(const T& val) : data(val) {}
private:
    T data;
 
    // 为这个 T 生成非模板 operator<< 
    friend std::ostream& operator<<(std::ostream& os, const Foo& obj)
    {
        return os << obj.data;
    }
};
 
int main()
{
    Foo<double> obj(1.23);
    std::cout << obj << '\n';
}

输出:

1.23

否则,函数模板需要在类体之前已声明为模板,这种情况下 Foo<T> 内的友元声明可以涉指 operator<< 对其 T 的全特化:

#include <iostream>
 
template<typename T>
class Foo; // 前置声明,以确保函数声明可行
 
template<typename T> // 声明
std::ostream& operator<<(std::ostream&, const Foo<T>&);
 
template<typename T>
class Foo
{
public:
    Foo(const T& val) : data(val) {}
private:
    T data;
 
    // 涉指对于这个特定的 T 的全特化 
    friend std::ostream& operator<< <> (std::ostream&, const Foo&);
 
    // 注意:这依赖于声明中的模板实参推导
    // 也可以通过 operator<< <T> 指定模板实参
};
 
// 定义
template<typename T>
std::ostream& operator<<(std::ostream& os, const Foo<T>& obj)
{
    return os << obj.data;
}
 
int main()
{
    Foo<double> obj(1.23);
    std::cout << obj << '\n';
}

链接

友元函数声明中不允许使用存储类说明符

如果在友元声明中首次声明并定义了一个函数或函数模板,并且外围类在某条导出声明中定义,那么它的名字具有与外围类的名字相同的链接。

(C++20 起)

否则, (C++20 起)如果在友元声明中声明了一个函数或函数模板,并且对应的非友元声明可及,那么它的名字与先前声明的名字具有相同的链接。

否则,友元声明引入的名字的链接与通常情况下一样。

注解

友元关系不传递(你朋友的朋友不是你的朋友)。

友元关系不继承(你朋友的孩子不是你的朋友,你的朋友不是你孩子的朋友)。

访问说明符对于友元声明的含义没有影响(它们可以在 private:public: 区间出现,且没有区别)。

友元类声明不能定义新的类(friend class X {}; 是错的)。

当局部类将一个无限定的函数或类声明为其友元时,只查找在其最内层非类作用域中的函数与类,而非全局函数:

class F {};
 
int f();
 
int main()
{
    extern int g();
 
    class Local // main() 函数中的局部类
    {
        friend int f(); // 错误,main() 中没有声明该函数
        friend int g(); // OK,main() 中有 g 的声明
        friend class F; // 令局部 F(随后定义)为友元
        friend class ::F; // 令全局 F 为友元
    };
 
    class F {}; // 局部 F
}

首次在类或类模板 X 中的友元声明中被声明的名字会成为 X 的最内层外围命名空间的成员,但它对于查找不可见(除了考虑 X实参依赖查找),除非在命名空间作用域中提供与之匹配的声明——细节见命名空间

功能特性测试宏 标准 功能特性
__cpp_variadic_friend 202403L (C++26) 可变参数友元声明

关键词

friend

示例

流插入与提取运算符往往声明为非成员友元:

#include <iostream>
#include <sstream>
 
class MyClass
{
    int i;                   // 友元能够访问非公开的非静态
    static inline int id{6}; // 和静态(可能内联的)成员
 
    friend std::ostream& operator<<(std::ostream& out, const MyClass&);
    friend std::istream& operator>>(std::istream& in, MyClass&);
    friend void change_id(int);
public:
    MyClass(int i = 0) : i(i) {}
};
 
std::ostream& operator<<(std::ostream& out, const MyClass& mc)
{
    return out << "MyClass::id = " << MyClass::id << "; i = " << mc.i;
}
 
std::istream& operator>>(std::istream& in, MyClass& mc)
{
    return in >> mc.i;
}
 
void change_id(int id) { MyClass::id = id; }
 
int main()
{
    MyClass mc(7);
    std::cout << mc << '\n';
//  mc.i = 333*2;  // 错误: i 是私有成员
    std::istringstream("100") >> mc;
    std::cout << mc << '\n';
//  MyClass::id = 222*3;  // 错误: id 是私有成员
    change_id(9);
    std::cout << mc << '\n';
}

输出:

MyClass::id = 6; i = 7
MyClass::id = 6; i = 100
MyClass::id = 9; i = 100

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

缺陷报告 应用于 出版时的行为 正确行为
CWG 45 C++98 T 的友元类的嵌套类的成员对 T 没有特殊访问权 嵌套类拥有和外围类相同的访问权
CWG 500 C++98 T 的友元类不能继承自 T 的私有与受保护成员,但其嵌套类可以 都可以继承自此类成员
CWG 1439 C++98 关于非局部类的友元声明没有覆盖到模板声明 已覆盖
CWG 1477 C++98 首次在类或类模板中的友元声明中被声明的名字在与之匹配的
声明在其他命名空间作用域提供的情况下对于查找不可见
此时它们对于查找可见
CWG 1804 C++98 当类模板的成员是友元时,类模板的部分特化的特化
的对应成员不是授予友元关系的类的友元
这些成员也是友元
CWG 2379 C++11 指代模板函数的全特化友元声明可以声明为 constexpr 已禁止
CWG 2588 C++98 由友元声明引入的名字的链接不明确 使之明确

引用

  • C++23 标准(ISO/IEC 14882:2024):
  • 11.8.4 Friends [class.friend]
  • 13.7.5 Friends [temp.friend]
  • C++20 标准(ISO/IEC 14882:2020):
  • 11.9.3 Friends [class.friend]
  • 13.7.4 Friends [temp.friend]
  • C++17 标准(ISO/IEC 14882:2017):
  • 14.3 Friends [class.friend]
  • 17.5.4 Friends [temp.friend]
  • C++14 标准(ISO/IEC 14882:2014):
  • 11.3 Friends [class.friend]
  • 14.5.4 Friends [temp.friend]
  • C++11 标准(ISO/IEC 14882:2011):
  • 11.3 Friends [class.friend]
  • 14.5.4 Friends [temp.friend]
  • C++98 标准(ISO/IEC 14882:1998):
  • 11.3 Friends [class.friend]
  • 14.5.3 Friends [temp.friend]

参阅

类类型 定义保有数个数据成员的类型
访问说明符 定义类成员的可见性