C++模板友元魔法【合法的访问私有成员】

模板友元魔法

  • 前置知识
    • 1. 编译器在模板实例化的时候会定义友元函数
    • 2. 【关键】模板显式实例化时不检查访问控制规则(public、protected、private)
  • 通过【显式实例化模板】+【友元函数在模板实例化时生成】 = 【访问私有成员变量】
    • 示例代码
    • 代码解释
  • 风险
  • 拓展


本文回答了这样一个问题:
如何在编译器规则允许范围内,在类外合法访问类的私有成员(包括成员变量和成员函数)?

前置知识

1. 编译器在模板实例化的时候会定义友元函数

template<typename T>
struct Foo {
  friend void bar() { cout << "Got it!" << endl; }
};

 错误做法
void bar();

...
bar();	// error - unresolved external symbol
...

 正确做法
template struc Foo<int>;
bar();//OK显式声明,使其在外部可以使用

注意!在这个例子中,不能像下边这样

template struc Foo<int>;
template struc Foo<double>;
//这会导致ODR不满足问题!

这是因为template struc Foo;template struc Foo;都会定义void bar()函数,这导致了多定义问题。(C++的唯一定义规则C++ODR)

因此,当我们实例化模板的时候,如果友元函数在模板中定义了(这里的定义指的是代码上的定义),那么模板的友元函数也会被同时定义出来(这里的定义指的是编译器对函数的定义)。但是还需要声明,才能访问到这个友元函数。

2. 【关键】模板显式实例化时不检查访问控制规则(public、protected、private)

C++标准草稿原文:

The usual access checking rules do not apply to names used to specify explicit instantiations. [ Note: In particular, the template arguments and names used in the function declarator (including parameter types, return types and exception specifications) may be private types or objects which would normally not be accessible and the template may be a member template or member function which would not normally be accessible. — end note ]

简述:

在进行模板的显式实例化时,通常的访问控制规则(如 public、protected、private)不适用于用于指定实例化的名称。

class MyClass
{
private:
	struct PrivateType{};
	
public:
	template<typename T>
	void func();

};

template void MyClass::func<MyClass::PrivateType>();

这里MyClass::PrivateType是一个私有的结构体,但是编译器允许这种行为。

通过【显式实例化模板】+【友元函数在模板实例化时生成】 = 【访问私有成员变量】

示例代码

#include 
#include 

namespace PrivateMemGetUtils {
    template <auto>
    struct A;

    template <typename T, typename Class, T Class::*Member>
    struct A<Member> {
        friend T& get(Class& c) { return c.*Member; }
    };

}  // namespace PrivateMemGetUtils

class Data {
  public:
    std::string value() const { return value_; }

  private:
    std::string value_ = "downdemo";
};

template struct PrivateMemGetUtils::A<&Data::value_>;
std::string& PrivateMemGetUtils::get(Data&);

int main() {
    Data data;
    assert(data.value() == "downdemo");
    PrivateMemGetUtils::get(data) = "june";
    assert(data.value() == "june");
}

代码解释

#include 
#include 

// 命名空间(有没有都行)
namespace PrivateMemGetUtils {
		//主模板声明,为下边模板特化打基础
    template <auto>
    struct A;
		
		// 以成员变量指针为模板参数的特化模板
    template <typename T, typename Class, T Class::*Member>
    struct A<Member> {
        friend T& get(Class& c) { return c.*Member; }
    };

}  // namespace PrivateMemGetUtils

// 实例类
class Data {
  public:
    std::string value() const { return value_; }

  private:
	  // 私有成员变量
    std::string value_ = "downdemo";
};

// 显式实例化结构体A的Data::value_成员变量指针版本。
//     注意到这里用了A<&Data::value_>,本身Data::value_是私有成员变量,不应该能够访问到。
//     但是,因为【模板显式实例化时不检查访问控制规则】,所以这里是合法的,因为编译器不会检查访问权限。
//     如果是直接实例化对象,那对不起非法了,例如 `PrivateMemGetUtils::A<&Data::value_> a; //非法`

// 此时,因为【模板实例化的时候会定义友元函数】,编译器会生成 std::string& get(Data& c) { return c.*(&Data::value_);}
// 不过此时get函数在A<&Data::value_> 类的作用域范围内,对外界不可见。
template struct PrivateMemGetUtils::A<&Data::value_>;

// 友元函数声明,使得可以直接通过PrivateMemGetUtils::get使用。
std::string& PrivateMemGetUtils::get(Data&);

int main() {
		// 定义对象data
    Data data;
    // 此时data的私有成员变量value_为默认值"downdemo" 
    assert(data.value() == "downdemo");
    // 对data.value_进行修改(因为get返回的是引用)
    PrivateMemGetUtils::get(data) = "june";
    // data.value_被改成了"june",修改私有成员变量成功!
    assert(data.value() == "june");
}

风险

  • 虽然C++标准没有明确禁止这种行为,但是也不提倡,因为这会破坏封装性。
  • 在一些底层的实现中可能会有这种技术。

拓展

  • 访问私有成员函数也可以。

你可能感兴趣的:(C++,c++,开发语言,模板)