允许访问私有成员

5

这个问题有点类似于我之前发布的这个问题。

我的目标是:允许在派生类B中访问基类A的私有成员,但需要遵守以下限制:

  • 我要访问的是一个结构体——实际上是一个std::map<>,而不是一个方法;
  • 不能修改基类;
  • 基类A没有模板方法可供我重载作为后门替代方案——并且我也不会添加这样的方法,因为这将违反第二个限制。

作为一种可能的解决方案,有人指向了litb的解决方案(帖子 / 博客),但是,我无论如何都无法理解这些帖子中所做的事情,因此,我无法针对自己的问题得出解决方案。

我想做什么:以下代码来自litb的解决方案,它提供了一种访问类/结构体的私有成员的方法,恰好涵盖了我提到的限制。

因此,我正在尝试重新排列这个代码:

template<typename Tag, typename Tag::type M>
struct Rob { 
  friend typename Tag::type get(Tag) {
    return M;
  }
};

// use
struct A {
  A(int a):a(a) { }
private:
  int a;
};

// tag used to access A::a
struct A_f { 
  typedef int A::*type;
  friend type get(A_f);
};

template struct Rob<A_f, &A::a>;

int main() {
  A a(42);
  std::cout << "proof: " << a.*get(A_f()) << std::endl;
}

我希望能有一种方法,让我能够做到以下几点——请注意,我将继承该类,因为在派生类B初始化后,std::map<>中的条目会立即添加,也就是说,std::map<>不仅仅是类A的静态成员,它具有默认值,所以我需要从特定的B实例中访问它:

// NOT MY CODE -- library <a.h>
class A {
private:
    std::map<int, int> A_map;
};

// MY CODE -- module "b.h"
# include <a.h>
class B : private A {
public:
    inline void uncover() {
        for (auto it(A_map.begin()); it != A_map.end(); ++it) {
            std::cout << it->first << " - " << it->second << std::endl;
        }
    }
};

我需要的答案:我非常希望在适当修改后,上面的代码能够正常工作,但如果能够解释litb解决方案中第一个代码块所做的操作,我也会非常满意。

为什么需要派生自己的解决方案?使用litb的代码作为库。你想要一个更易管理的版本吗?我曾经更好地封装了它。但是如果我没记错的话,我认为他实际上在最后有一个自包含的版本。 - Potatoswatter
@Potatoswatter 这里的问题是我甚至不知道这个解决方案是否适用于我的问题。我还没有能够将其与我的代码实际使用--主要是因为我还没有真正理解在libt的解决方案中所做的事情。 - Rubens
4个回答

7
很遗憾,这篇博客的代码有些不太清楚。但其概念很简单:显式模板实例化可以获得任何类的免费后台通行证,因为:
- 库类的显式实例化可能是客户类的实现细节,并且 - 显式实例化只能在命名空间作用域中声明。
自然的分配后台通行证的方式是作为成员指针。如果你有指向给定类成员的指针,你可以访问该类的任何对象,而不管访问限制如何。幸运的是,即使在C++03中,指向成员的指针也可以是编译时常量。
因此,我们希望在显式实例化时生成一个生成成员指针的类。
显式实例化只是定义类的一种方式。那么仅仅生成一个类怎么到某事呢?有两个选择:
- 定义一个不是类成员的friend函数。这就是litb所做的。 - 定义一个静态数据成员,在启动时初始化。这是我喜欢用的风格。
我将首先介绍我的风格,然后讨论它的缺点,然后修改它以匹配litb的机制。最终结果仍然比博客里的代码简单。

简单版本。

该类采用三个模板参数:受限成员的类型、其实际名称和一个引用全局变量以接收指向它的指针。该类安排静态对象进行初始化,其构造函数会初始化全局变量。
template< typename type, type value, type & receiver >
class access_bypass {
    static struct mover {
        mover()
            { receiver = value; }
    } m;
};

template< typename type, type value, type & receiver >
typename access_bypass< type, value, receiver >::mover
    access_bypass< type, value, receiver >::m;

使用方法:

type_of_private_member target::* backstage_pass;
template class access_bypass <
    type_of_private_member target::*,
    & target::member_name,
    backstage_pass
>;

target t;
t.* backstage_pass = blah;

查看它的工作情况。

不幸的是,在程序进入main之前,您不能依赖于其他源文件中全局对象构造函数的结果可用,因为没有标准的方法告诉编译器初始化文件的顺序。但是全局变量按照声明的顺序进行初始化,因此您只需将绕过放在顶部即可,只要静态对象构造函数不对其他文件进行函数调用。

强大版本。

这借鉴了litb代码中的一个元素,通过添加标记结构和friend函数,但这是一个小修改,我认为它仍然很清晰,不比上述代码差。

template< typename type, type value, typename tag >
class access_bypass {
    friend type get( tag )
        { return value; }
};

使用方法:

struct backstage_pass {}; // now this is a dummy structure, not an object!
type_of_private_member target::* get( backstage_pass ); // declare fn to call

// Explicitly instantiating the class generates the fn declared above.
template class access_bypass <
    type_of_private_member target::*,
    & target::member_name,
    backstage_pass
>;

target t;
t.* get( backstage_pass() ) = blah;

查看它的工作。

这个强大版本与litb的博客文章之间的主要区别在于,我已经将所有参数收集到一个地方,并使标记结构为空。它只是相同机制的更清晰的接口。但是您必须声明get函数,而博客代码会自动执行此操作。


不幸的是,您不能只是将绕过事物放在TU的顶部。由于静态数据成员是一个实例化实体,因此排序规则不适用。 - Johannes Schaub - litb

4

好的,你问如何将那个奇怪的“Rob”代码应用于你的使用情况中,下面是方法。

// the magic robber
template<typename Tag, typename Tag::type M>
struct Rob {
    friend typename Tag::type get(Tag) {
        return M;
    }
};

// the class you can't modify
class A {
private:
    std::map<int, int> A_map;
};

struct A_f {
    typedef std::map<int, int> A::*type;
    friend type get(A_f);
};

template struct Rob<A_f, &A::A_map>;

class B : private A {
public:
    inline void uncover() {
        std::map<int, int>::iterator it = (this->*get(A_f())).begin();
    }
};

现在,我个人认为,在滥用 C++ 方面,治愈方法可能比疾病更加严重。你可以自己决定,所以我已将其作为我的另一个答案发布,使用预处理器以老式的方式完成。
编辑:
工作原理:
在这里,我将复制上面的代码,但简化类型并绘制更多的代码,并附有大量注释。请注意,在进行此操作之前,我并不是非常了解该代码,现在也不能完全理解它,并且明天肯定不会记得它的工作原理。请维护者注意。
以下是我们不允许更改的代码,包括私有成员:
// we can use any type of value, but int is simple
typedef int value_type;

// this structure holds value securely.  we think.
struct FortKnox {
    FortKnox() : value(0) {}
private:
    value_type value;
};

现在开始进行盗劫:
// define a type which is a pointer to the member we want to steal
typedef value_type FortKnox::* stolen_mem_ptr;

// this guy is sort of dumb, but he knows a backdoor in the standard
template<typename AccompliceTag, stolen_mem_ptr MemPtr>
struct Robber {
    friend stolen_mem_ptr steal(AccompliceTag) {
        return MemPtr; // the only reason we exist: to expose the goods
    }
};

// this guy doesn't know how to get the value, but he has a friend who does
struct Accomplice {
    friend stolen_mem_ptr steal(Accomplice);
};

// explicit instantiation ignores private access specifier on value
// we cannot create an object of this type, because the value is inaccessible
// but we can, thanks to the C++ standard, use this value in this specific way
template struct Robber<Accomplice, &FortKnox::value>;

// here we create something based on secure principles, but which is not secure
class FortKnoxFacade : private FortKnox {
public:
    value_type get_value() const {
        // prepare to steal the value
        // this theft can only be perpetrated by using an accomplice
        stolen_mem_ptr accessor = steal(Accomplice()); // it's over now
        // dereference the pointer-to-member, using this as the target
        return this->*accessor;
    }
};

int main() {
    FortKnoxFacade fort;
    return fort.get_value();
}

+1 它可以工作了!\o/ 非常感谢!您介意浅显易懂地逐步解释一下这里所做的事情吗?我真的不太明白所有的 typename Tag::type Mfriend type get(A_F) 的含义。 - Rubens
1
我已经编辑了我的答案,增加了相当多的细节。然而,我做这件事有相当大的保留,因为我认为整个方法会让接手你代码的人咒骂你的名字——也许比你诅咒那个把你置于困境的库的作者的话更粗鲁。 - John Zwinck
@Rubens 和 John,看看我的答案,这是我之前想出来的一种更简单的方式来完成同样的任务。如果你们需要更详细的说明,请告诉我,但这个方法应该比原始的方法更易于理解,因为原始方法的数据流相当混乱。 - Potatoswatter
@potatoswatter,简单或清晰取决于观察者的眼光 :-) 你的版本可以更容易地解释,因为它不需要涵盖ADL。但成员的类型必须被提及两次。这并不像仅仅从基类派生标签类型那么简单(然后你会得到愚蠢的gcc警告,但可以通过解决)。话虽如此,我确实喜欢你的解释。 - Johannes Schaub - litb
@JohnZwinck和大家,非常感谢你们的耐心和非常有帮助的解释。希望我能接受所有答案,但我不能,所以我用我的伪随机化选择了一个答案。再次感谢!(: - Rubens
为什么在函数steal中需要“Accomplice”参数?我尝试删除函数steal中的参数Accomplice,但代码无法编译。它会抱怨“steal未在此范围内声明”。 - Casualet

3

还需要更加残酷的内容吗?

// MY CODE -- module "b.h"
# define private protected
# include <a.h>
# undef private
class B : private A {
    // now you can access "private" members and methods in A

3
这就是我们所说的未定义行为。 - Potatoswatter
1
@Rubens,这是必须的。预处理器将每个private替换为protected - 它没有“保留字”概念。在此之后,您的C++编译器会看到在应该是private的地方出现了protected。然后它将允许您从a.h访问所有私有方法/变量。嘿 - Aniket Inge
2
它可以重新排列数据,因为具有相同可访问性的连续数据成员必须位于递增地址上。 - Cheers and hth. - Alf
1
具体而言,如果在包含库头文件时启用了#define,那么它就是未定义行为;如果在一个TU中启用了某个头文件的#define宏,而在另一个TU中却没有启用,则很可能会导致违反单一定义规则。是的,这可能会导致数据结构被重新排列。 - Potatoswatter
2
如果头文件 <a.h> 定义了一个类,例如 class foo { int bar; }; ---没有使用 private 关键字--- 那么 bar 仍然是私有的。为了解决这个问题,还需要添加 #define class struct - comocomocomocomo
显示剩余9条评论

1
我知道的这个成语最好的表述方式如下:

最好的表述方式如下:

template<class Tag,typename Tag::type MemberPtr>
struct access_cast{
 friend typename Tag::type get(Tag){return MemberPtr;};
};

template<class Tag,class MemberPtr>
struct access_tag{
 typedef MemberPtr type;
 friend type get(Tag);
};

class A {
public:
 auto x() const {return x_;};
private: 
 int x_ = 9;
};

#include <iostream>

struct AMemTag: access_tag<AMemTag,int A::*>{}; //declare tag
template struct access_cast<AMemTag,&A::x_>; //define friend get function

int main() {
 A a;
 std::cout<<a.x()<<"\n";
 a.*get(AMemTag()) = 4; //dereference returned member pointer and modify value
 std::cout<<a.x()<<"\n";
}

看它如何工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接