在C++类的外部调用类的私有方法

引子

可否在C++类的外部调用类的私有方法呢?既然谈到了这个问题,当然是可以的。

问题

目标是在一个外部function中调用Widget::forbidden()这个private function。限制条件是不能修改Widget类。

class Widget {
   private:
    void forbidden();
  };
void hijack(Widget& w) {
    w.forbidden();  // ERROR!
  }

下面我们一步步来实现这个小目标:

技术准备

显然,我们不可能直接像w.forbidden()类似那样调用,我们必须通过PMF(pointer to member function)来间接调用,所以,下面先简要介绍PMF:

1. pointers to member functions

由于下面会广泛的用到pointers to member functions (PMFs),我们先来回顾一下它的用法:

class Calculator {
  float current_val = 0.f;
 public:
   void clear_value() { current_val = 0.f; };
   float value() const {
     return current_val;
   };

   void add(float x) { current_val += x; };
   void multiply(float x) { current_val *= x; };
};

在C++11中,使用函数指针调用函数,我们可以这么做:

  using Operation = void (Calculator::*)(float);
 
  Operation op1 = &Calculator::add;
  Operation op2 = &Calculator::multiply;
  
  using Getter = float (Calculator::*)() const;
  Getter get = &Calculator::value;
  
  Calculator calc{};
  (calc.*op1)(123.0f); // Calls add
  (calc.*op2)(10.0f);  // Calls multiply
  // Prints 1230.0
  std::cout << (calc.*get)() << '\n';

函数指针的一个特性是它可以绑定到类的是由成员函数,下面我们将会用到这点,假设Widget类提供了某种机制来获取其私有成员函数的方法,那么,实现我们的目标就可以像下面这样做:

class Widget {
 public:
  static auto forbidden_fun() {
    return &Widget::forbidden;
  }
 private:
  void forbidden();
};

void hijack(Widget& w) {
  using ForbiddenFun = void (Widget::*)();
  ForbiddenFun const forbidden_fun =
    Widget::forbidden_fun();

  // Calls a private member function on the Widget
  // instance passed in to the function.
  (w.*forbidden_fun)();
}

采用这种办法不错,但是别忘了,我们不能修改Widget类,大多数的类也不提供返回私有成员的方法。

既然我们不能通过添加函数返回PMF的方法来进行实践,那么,是否还有其它方法可以在类的外部访问类的私有成员呢?C++提供了显式模板特化的方法。

2. The explicit template instantiation

C++ 标准中提到:

17.7.2 (item 12)
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.]

显式模板特化时,名字访问规则此时不起作用,Stack Overflow 给出了一个例子:

class Foo
{
private:
  struct Bar;

  template<typename T> class Baz { };

public:
  void f();  // does things with Baz<Bar>
};

// explicit instantiation declaration
extern template class Foo::Baz<Foo::Bar>;

这里在显式模板特化Foo::Baz时,可以访问Foo::Baz和Foo::bar

到这里,既然成员访问规则不适用于模板特化,模板特化时可以访问类的私有成员,那么,我们可否将PMF作为模板参数呢,下面将介绍这点。

3. Passing a member-function pointer as a non-type template parameter

在C++中模板参数通常分为两类:

  • 类型参数,一般的参数都是类型参数,用来进行类型替换;
  • 非类型参数,常见的是整型或指针类型的非类型参数

比如下面,其中T是类型参数,size是非类型参数:

template <class T, int size> // size is the non-type parameter
class StaticArray
{
private:
    // The non-type parameter controls the size of the array
    T m_array[size];
 
public:
    T* getArray();
	
    T& operator[](int index)
    {
        return m_array[index];
    }
};

下面我们再看一个指针作为非类型参数的例子:

class SpaceShip {
 public:
  void dock();
  // ...
};

// Member function alias that matches the
// signature of SpaceShip::dock()
using SpaceShipFun = void (SpaceShip::*)();

// spaceship_fun is a pointer-to-member-function
// value which is baked-in to the type of the
// SpaceStation template at compile time.
template <SpaceShipFun spaceship_fun>
class SpaceStation {
  // ...
};

// Instantiate a SpaceStation and pass in a
// pointer to member function statically as a
// template argument.
SpaceStation<&SpaceShip::dock> space_station{};

上面的别名SpaceShipFun的使用使得SpaceStation只能用SpaceShip::dock的PMF来实例化,这个模板显得不那么通用,所以我们可以将函数指针也作为一个模板参数:

template <
  typename SpaceShipFun,
  SpaceShipFun spaceship_fun
>
class SpaceStation {
  // ...
};

// Now we must also pass the type of the pointer to
// member function when we instantiate the
// SpaceStation template.
SpaceStation<
  void (SpaceShip::*)(),
  &SpaceShip::dock
> space_station{};

当然,我们也可以更进一步,在实例化模板的时候让编译器自动推导函数指针的类型:

 SpaceStation<
    decltype(&SpaceShip::dock),
    &SpaceShip::dock
  > space_station{};

到这里,我们似乎找到了解决方案,通过显示模板特化导出本来对外部不可见的函数指针,通过函数指针调用是由函数

Solution. Passing a private pointer-to-member-function as a template parameter

我们结合上面的3个技巧,似乎找到了解决方案:

// The first template parameter is the type
// signature of the pointer-to-member-function.
// The second template parameter is the pointer
// itself.
template <
  typename ForbiddenFun,
  ForbiddenFun forbidden_fun
> struct HijackImpl {
  static void apply(Widget& w) {
    // Calls a private method of Widget
    (w.*forbidden_fun)();
  }
};

// Explicit instantiation is allowed to refer to
// `Widget::forbidden` in a scope where it's not
// normally permissible.
template struct HijackImpl<
  decltype(&Widget::forbidden),
  &Widget::forbidden
>;

void hijack(Widget& w) {
    HijackImpl<
      decltype(&Widget::forbidden),
      &Widget::forbidden
    >::apply(w);
  }

运行一下,发现其实不可行:

error: 'forbidden' is a private member of 'Widget'
   HijackImpl<decltype(&Widget::forbidden),
     &Widget::forbidden>::hijack(w);

主要的原因是因为在hijack函数中使用HijackImpl<decltype(&Widget::forbidden), &Widget::forbidden>::apply(w)不是显式模板特化,它只是常见的隐式模板实例化。

那么,如何采用显示模板特化的方法你?这就需要使用friend技巧:

5. Friend

我们通常这样定义和使用友员函数:

class Gadget {
  // Friend declaration gives `frobnicate` access
  // to Gadget's private members.
  friend void frobnicate();

 private:
  void internal() {
    // ...
  }
};

// Definition as a normal free function
void frobnicate() {
  Gadget g;
  // OK because `frobnicate()` is a friend of
  // `Gadget`.
  g.internal();
}

但是不会这样:

class Gadget {
  // Free function declared as a friend of Gadget
  friend void frobnicate() {
    Gadget g;
    g.internal(); // Still OK
  }

 private:
   void internal();
};

void do_something() {
  // NOT OK: Compiler can't find frobnicate()
  // during name lookup
  frobnicate();
}

因为frobnicate在Gadget内部,do_something()在做名字查找时不会查找到frobnicate,解决办法如下:

class Gadget {
  friend void frobnicate(Gadget& gadget) {
    gadget.internal();
  }

 private:
   void internal();
};

void do_something(Gadget& gadget) {
  // OK: Compiler is now able to find the
  // definition of `frobnicate` inside Gadget
  // because ADL adds it to the candidate set for
  // name lookup.
  frobnicate(gadget);
}
			

当然如果do_something不带参数,我们在外部重新声明友员函数,也是可以的:

class Gadget {
  // Definition stays inside the Gadget class
  friend void frobnicate() {
    Gadget g;
    g.internal();
  }

 private:
   void internal();
};

// An additional namespace-scope declaration makes
// the function available for normal name lookup.
void frobnicate();

void do_something() {
  // The compiler can now find the function
  frobnicate();
}

了解了这些,对下面这样的代码就不会太吃惊了:

#include <iostream>

template <int N>
class SpookyAction {
  friend int observe() {
    return N;
  }
};

int observe();

int main() {
  SpookyAction<42>{};
  std::cout << observe() << '\n';  // Prints 42
}

Put the magic pieces together

那么结合上面所有的这些技巧,我们就可以得到下面的代码:

namespace {
// This is a *different* type in every translation
// unit because of the anonymous namespace.
struct TranslationUnitTag {};
}

void hijack(Widget& w);

template <
  typename Tag,
  typename ForbiddenFun,
  ForbiddenFun forbidden_fun
> class HijackImpl {
  friend void hijack(Widget& w) {
    (w.*forbidden_fun)();
  }
};

// Every translation unit gets its own unique
// explicit instantiation because of the
// guaranteed-unique tag parameter.
template class HijackImpl<
  TranslationUnitTag,
  decltype(&Widget::forbidden),
  &Widget::forbidden
>;

完整代码可以参见这里:@wandbox

总结一下:

  1. 我们通过显式模板特化,将Widget::forbidden的函数指针传递给了HijackImpl示例,特化了模板函数hijack
  2. 通过友员函数声明,让hijack函数可以在他处被调用,从而达成了小目标

结语

写这些,是希望你将上面的代码用于实际产品代码吗?绝对不要这么做! 不要违背C++的封装的原则,一旦打开潘多拉魔盒,那么你的产品代码虽然看起来可运行,但是将不可维护,不可知,因为你破坏了我们和C++语言以及程序员之间的合约。

写这些的目的主要是怀着好奇的目的,提高对C++语言的了解。


Reference:

  1. https://accu.org/journals/overload/28/156/harrison_2776/
  2. http://www.gotw.ca/gotw/076.htm
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值