【问题标题】:In C++11, protected means public?在 C++11 中,受保护意味着公共?
【发布时间】:2013-06-03 00:58:03
【问题描述】:

继续在C++ error: base function is protected 中学到的东西...

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;
}

真的符合标准吗?

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


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

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

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


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


也相关:GotW 76: Uses and Abuses of Access Rights

【问题讨论】:

  • 是的,我看不出 C++11 中剥离了哪些保护。
  • @Charles:我并不是说 C++98 没有同样的漏洞。只是这存在于当前版本中。
  • @BenVoigt:也可以在不强制转换的情况下抢劫private 成员
  • @Andy:你能提供一个例子(不调用未定义的行为)吗?
  • @Ben:Johannes Schaub 想出了这个技巧。这是link

标签: c++ member member-access


【解决方案1】:

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

m 是类Base 的成员时,使&amp;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
}

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

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

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

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

这不好。

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

04/00 会议记录:

目前处理的基本原理是允许最广泛的 可能使用给定的成员地址表达式。自从 指向基成员的指针可以隐式转换为 指向派生成员的指针,使表达式的类型为 指向基成员的指针允许初始化或分配结果 指向基成员的指针或派生成员的指针。 接受此提议将只允许后者使用。

【讨论】:

  • 但它允许您操作Encapsulated 的任何子类的对象,即使是那些阻止进一步继承的对象!我无法想象这是正确的,即使它是符合的。
  • @Ben Voigt:没有争议,这看起来确实是一个巨大的妥协。
  • WRT 你的编辑,重点是使指向成员的指针无法与Base 对象一起使用。如果你想要一个指向成员的指针Base,请使用&amp;Base::m
  • 我明白你的意思。另外,请参阅此处open-std.org/jtc1/sc22/wg21/docs/cwg_closed.html#203。他们有更有说服力的激励例子。
  • 感谢您的链接。据我所知,所有这些例子都支持改变(它们在当前规则下被打破)。所以我不同意你倒数第二句话。它们只是进一步证明最初的决定没有经过深思熟虑。
【解决方案2】:

关于 C++ 中的访问说明符,需要牢记的主要是它们控制 name 的使用位置。它实际上并没有做任何事情来控制对对象的访问。在 C++ 上下文中,“访问成员”是指“使用名称的能力”。

观察:

class Encapsulator {
  protected:
    int i;
};

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

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

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


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

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

【讨论】:

  • 不,访问说明符不控制名称的使用位置。重载解决发生在访问检查之前。其次,如果&amp;Gimme::i 具有int Gimme::* 类型,那么它仍然可以访问Encapsulator::i,但只能在Gimme 类型(或某个子类)的对象中访问。如果您的解释是正确的,那么我在问题中加粗的 14.4p1 中的文本将不存在。
  • @BenVoigt 是的,重载解决方案是第一位的;通过“访问说明符控制可以在何处使用名称”,我的意思是,一旦重载决议确定了您尝试使用的名称,访问说明符将确定您是否可以使用该名称。其次,再次阅读您加粗的文本,但要了解“成员”与“子对象”的含义不同;短语“访问受保护成员”适用于名称Encapsulator::i,但同时不适用于名称Gimmie::i,因为它们是不同的成员。 &amp;Gimmie::i 的类型不影响这个
  • 但是&amp;Gimme::i 的类型会影响指针以后可以使用的对象。我完全同意,如果Encapsulator 返回一个指向其成员子对象的指针,那么任何代码都可以取消引用该指针而无需进一步检查。但是Gimme 不应该能够获得一个指向成员的指针,它通常适用于所有Encapsulator 对象......为了与粗体文本一致,&amp;Gimme::i 应该返回一个只能应用于Gimme 对象。
  • @BenVoigt 可以使用公众会员Gimmie::i这一事实与粗体字不矛盾;粗体文本仅适用于受保护的成员。使用这个公共成员的结果是否应该与基类或从同一基类派生的其他类一起使用是一个完全独立的设计决策,与粗体文本无关。
  • Gimme::i进行访问检查,然后访问非Gimme类型的对象,这与粗体文本不一致。
猜你喜欢
  • 2023-01-27
  • 2011-09-02
  • 2011-02-08
  • 2012-02-13
  • 2023-04-08
  • 1970-01-01
  • 2015-10-26
  • 2011-06-19
  • 2012-12-17
相关资源
最近更新 更多