13

继续在 C++ 错误中学到的东西:基函数受到保护...

C++11 指向成员的指针规则有效地去除了protected任何值的关键字,因为可以在不相关的类中访问受保护的成员,而无需任何邪恶/不安全的强制转换。

以机智:

class Encapsulator
{
  protected:
    int i;
  public:
    Encapsulator(int v) : i(v) {}
};

Encapsulator f(int x) { return x + 2; }

#include <iostream>
int main(void)
{
    Encapsulator e = f(7);
    // forbidden: std::cout << e.i << std::endl; because i is protected
    // forbidden: int Encapsulator::*pi = &Encapsulator::i; because i is protected
    // forbidden: struct Gimme : Encapsulator { static int read(Encapsulator& o) { return o.i; } };

    // loophole:
    struct Gimme : Encapsulator { static int Encapsulator::* it() { return &Gimme::i; } };
    int Encapsulator::*pi = Gimme::it();
    std::cout << e.*pi << std::endl;
}

真的符合标准吗?

(我认为这是一个缺陷,并声称即使是基类的成员,它的类型也&Gimme::i确实应该是。但我在标准中没有看到任何使它如此的东西,并且有一个非常具体的示例说明了这一点。)int Gimme::*i


我意识到有些人可能会对第三种评论方法(第二个 ideone 测试用例)实际上失败感到惊讶。那是因为思考受保护的正确方法不是“我的派生类可以访问,而没有其他人”,而是“如果您从我那里派生,您将可以访问您的实例中包含的这些继承变量,除非您授予它”。例如,如果Buttoninherits ,则实例内的Control受保护成员只能访问, 和, 和(假设不禁止)实例的实际动态类型和任何中间基。ControlButtonControlButtonButton

这个漏洞颠覆了那个合同,完全违背了规则 11.4p1 的精神:

当非静态数据成员或非静态成员函数是其命名类的受保护成员时,将应用超出第 11 条中所述的附加访问检查。如前所述,授予对受保护成员的访问权限是因为引用发生在某个类的朋友或成员中C。如果访问要形成指向成员的指针(5.3.1),则嵌套名称说明符应表示C或派生自 的类C。所有其他访问都涉及(可能是隐式的)对象表达式。在这种情况下,对象表达式的类应该是C或派生自的类C


感谢 AndreyT 链接http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_closed.html#203,它提供了更多激励改变的例子,并呼吁由 Evolution 提出这个问题工作小组。


也相关:GotW 76:访问权限的使用和滥用

4

2 回答 2

11

我已经看到了这种技术,我称之为“受保护的黑客”,在这里和其他地方多次提到。是的,这种行为是正确的,并且它确实是一种绕过受保护访问的合法方式,而无需诉诸任何“肮脏”的黑客行为。

m是类的成员时Base,使&Derived::m表达式产生Derived::*类型指针的问题是类成员指针是逆变的,而不是协变的。它会使生成的指针无法用于Base对象。例如,此代码编译

struct Base { int m; };
struct Derived : Base {};

int main() {
  int Base::*p = &Derived::m; // <- 1
  Base b;
  b.*p = 42;                  // <- 2
}

因为&Derived::m产生一个int Base::*值。如果它产生一个int Derived::*值,代码将无法在第 1 行编译。如果我们尝试使用

  int Derived::*p = &Derived::m; // <- 1

它将无法在第 2 行编译。使其编译的唯一方法是执行强制转换

  b.*static_cast<int Base::*>(p) = 42; // <- 2

这不好。

PS我同意,这不是一个很有说服力的例子(“&Base:m从一开始就使用,问题就解决了”)。但是,http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_closed.html#203有更多信息可以解释为什么最初做出这样的决定。他们说

04/00 会议记录:

当前处理的基本原理是允许尽可能广泛地使用给定的成员地址表达式。由于指向基成员的指针可以隐式转换为指向派生成员的指针,因此将表达式的类型设置为指向基成员的指针允许结果初始化或分配给指针- to-base-member 或指向派生成员的指针。接受这个提议将只允许后者使用。

于 2013-06-06T20:51:51.340 回答
5

关于 C++ 中的访问说明符,要记住的主要事情是它们控制可以在何处使用名称。它实际上并没有做任何事情来控制对对象的访问。在 C++ 的上下文中,“访问成员”意味着“使用名称的能力”。

观察:

class Encapsulator {
  protected:
    int i;
};

struct Gimme : Encapsulator {
    using Encapsulator::i;
};

int main() {
  Encapsulator e;
  std::cout << e.*&Gimme::i << '\n';
}

这 ,e.*&Gimme::i是允许的,因为它根本不访问受保护的成员。我们正在访问Gimmeusing声明在内部创建的成员。也就是说,即使一个using声明在实例中没有暗示任何额外的子对象Gimme,它仍然会创建一个额外的成员成员和子对象不是一回事Gimmie::i是不同的公共成员,可用于访问与受保护成员相同的子对象Encapsulator::i


一旦理解了“类成员”和“子对象”之间的区别,就应该清楚这实际上不是 11.4 p1 规定的合同的漏洞或意外失败。

可以为其他无法命名的对象创建可访问的名称或以其他方式提供对这些对象的访问是预期的行为,即使它与某些其他语言不同并且可能令人惊讶。

于 2013-06-06T22:32:06.343 回答