使用朋友类封装C ++中的私有成员函数-好的做法还是滥用?


12

因此,我注意到可以通过执行以下操作来避免将私有函数放在标头中:

// In file pred_list.h:
    class PredicateList
    {
        int somePrivateField;
        friend class PredicateList_HelperFunctions;
    public:
        bool match();
    } 

// In file pred_list.cpp:
    class PredicateList_HelperFunctions
    {
        static bool fullMatch(PredicateList& p)
        {
            return p.somePrivateField == 5; // or whatever
        }
    }

    bool PredicateList::match()
    {
        return PredicateList_HelperFunctions::fullMatch(*this);
    }

私有函数永远不会在标头中声明,并且导入标头的类的使用者永远不需要知道它的存在。如果helper函数是模板,则这是必需的(替代方法是将完整的代码放在标头中),这就是我“发现”它的方式。如果添加/删除/修改私有成员函数,则不需要重新编译包含头文件的每个文件的另一个好处。所有专用功能都位于.cpp文件中。

所以...

  1. 这是一个众所周知的设计模式吗?
  2. 对我来说(来自Java / C#背景并且自己学习C ++),这似乎是一件好事,因为标头定义了接口,而.cpp定义了实现(改进的编译时间是不错的奖金)。但是,它也闻起来像是滥用了不打算以这种方式使用的语言功能。那是什么呢?在专业的C ++项目中,您会不会皱眉呢?
  3. 我没有想到的陷阱吗?

我知道Pimpl,这是一种在库边缘隐藏实现的更强大的方法。这更适合与内部类一起使用,在内部类中,Pimpl会导致性能问题,或者因为类需要被视为值而无法工作。


编辑2:Dragon Energy在下面的出色回答中提出了以下解决方案,该解决方案根本不使用friend关键字:

// In file pred_list.h:
    class PredicateList
    {
        int somePrivateField;
        class Private;
    public:
        bool match();
    } 

// In file pred_list.cpp:
    class PredicateList::Private
    {
    public:
        static bool fullMatch(PredicateList& p)
        {
            return p.somePrivateField == 5; // or whatever
        }
    }

    bool PredicateList::match()
    {
        return PredicateList::Private::fullMatch(*this);
    }

这样可以避免的震荡因子friend(似乎已经妖魔化了goto),同时仍然保持相同的分离原理。


2
“用户可以定义自己的类PredicateList_HelperFunctions,并允许他们访问私有字段。 ”那不是违反ODR吗?您和使用者都必须定义相同的类。如果这些定义不相等,则代码格式错误。
Nicol Bolas

Answers:


13

至少说出您已经认识到的东西有点深奥,当我第一次开始遇到您的代码时想知道您在做什么以及这些帮助程序类在哪里实现,直到我开始选择您的样式之前,您可能已经scratch了一下头/ habits(这时我可能会完全习惯)。

我确实喜欢您减少标题中的信息量。尤其是在非常大的代码库中,这可能对减少编译时的依赖性和最终的构建时间具有实际效果。

但是我的直觉是,如果您需要以这种方式隐藏实现细节,则希望将参数传递给源文件中具有内部链接的独立函数。通常,您可以实现对实现特定类有用的实用程序函数(或整个类),而无需访问类的所有内部,而是仅将相关的内容从方法的实现传递给函数(或构造函数)。当然,这样做的好处是可以减少班级与“帮助者”之间的耦合。如果您发现他们已经开始服务于适用于多个类实现的更为通用的目的,那么它也倾向于进一步将原本可能是“帮助者”的东西进一步推广。

当我在代码中看到很多“助手”时,有时也会有些畏缩。这并不总是正确的,但有时他们可能是开发人员的症状,该开发人员只是随意分解函数以消除代码重复,因为大量的数据块传递给名称/目的几乎无法理解的函数,而事实是它们减少了实现某些其他功能所需的代码。稍微多花一点时间,事先就可以将类的实现分解为更多函数的思路变得更加清晰,并且倾向于将特定参数传递给对象的整个实例,并可以完全访问内部函数推广这种设计思想风格。我当然不建议您这样做(我不知道),

如果那变得难以处理,我会考虑第二种更惯用的解决方案,即pimpl(我意识到您提到了它的问题,但我认为您可以将解决方案归纳为最省力的方法)。这样可以将您班级需要实现的大量信息(包括其私有数据)转移到标头批发之外。pimpl的性能问题可以通过像免费列表之类的便宜的恒定时间分配器来缓解,同时保留值的语义,而无需实现成熟的用户定义的复制控制器。

  • 对于性能方面,pimpl至少确实会引入指针开销,但是我认为在实际操作中,情况必须非常严重。如果空间局部性没有通过分配器显着降低,则您在对象上进行的紧密循环(如果性能很受关注,通常应该是同质的),在实践中,如果您使用一个空闲列表来分配pimpl,将类的字段放入很大程度上连续的内存块中。

我个人只有在穷尽这些可能性之后才考虑这样的事情。我确实认为这是一个不错的主意,如果替代方案像是暴露于标头的更多私有方法,而实际上只有其深奥的性质才是现实问题。

替代

突然出现在我脑海中的另一个选择,可以在没有朋友的情况下大致实现您的相同目的:

struct PredicateListData
{
     int somePrivateField;
};

class PredicateList
{
    PredicateListData data;
public:
    bool match() const;
};

// In source file:
static bool fullMatch(const PredicateListData& p)
{
     // Can access p.somePrivateField here.
}

bool PredicateList::match() const
{
     return fullMatch(data);
}

现在这似乎是一个非常微妙的区别,我仍将其称为“帮助程序”(可能贬义的意思是,无论我们是否需要全部将类的整个内部状态传递给函数,)除了它确实避免了遇到的“震惊”因素friend。总的来说friend,经常不进行进一步检查似乎有点吓人,因为它表示您可以在其他地方访问您的类内部(这暗示着它可能无法维护自己的不变式)。friend如果人们意识到这种做法,那么使用它的方式就会变得毫无意义,因为friend只是驻留在同一个源文件中,以帮助实现该类的私有功能,但是以上内容至少在一个不争的事实中实现了几乎相同的效果,即它不涉及任何避免这种情况的朋友(“射击,这个班级有一个朋友。其他班级的其他人在哪里可以访问/变异?”)。紧随其后的版本立即表明,在实现时所做的任何事情都无法访问/更改私有变量PredicateList

如此细微的差别可能正在朝着教条化的领域发展,因为任何人都可以迅速弄清是否您统一命名事物*Helper*并将它们全部放入同一个源文件中,该源文件作为一个类的私有实现的一部分捆绑在一起。但是,如果我们挑剔,那么上面的样式可能会在没有friend关键词的情况下乍一看不会引起太多的下意识的反应。

对于其他问题:

使用者可以定义自己的类PredicateList_HelperFunctions,并允许他们访问私有字段。尽管我认为这不是一个大问题(如果您真的想要在那些私有领域进行一些铸造),也许它会鼓励消费者以这种方式使用它?

这可能跨越API边界,在这种情况下,客户端可以使用相同的名称定义第二个类,并以这种方式获得对内部的访问而不会发生链接错误。再说一次,我主要是从事图形处理的C编码器,其在“如果...”级别的安全问题在优先级列表中非常低,因此,诸如此类的问题只是我倾向于挥手并跳舞和尝试假装它们不存在。:-D如果您在一个类似此类问题非常严重的领域工作,我认为这是一个不错的考虑因素。

上述替代方案也避免了这个问题。如果仍然要坚持使用friend,也可以通过将帮助程序设置为私有嵌套类来避免该问题。

class PredicateList
{
    ...

    // Declare nested class.
    class Helper;

    // Make it a friend.
    friend class Helper;

public:
    ...
};

// In source file:
class PredicateList::Helper
{
    ...
};

这是一个众所周知的设计模式吗?

据我所知。我有点怀疑会不会有一个,因为它确实涉及实现细节和样式的细节。

“助手地狱”

我要求进一步澄清一点,即当我看到带有许多“帮助”代码的实现时有时会畏缩,这可能与某些观点有些争议,但实际上是事实,因为我调试某些调试器时确实感到畏缩我的同事实施一个类的过程只是为了找到“帮手”。:-D我不是团队中唯一想弄清楚所有这些助手应该做什么的人。我也不想像“你不使用助手”那样教条主义但我会提出一个小小的建议,那就是在实践中考虑如何实现缺少的东西可能会有所帮助。

不是所有的私有成员函数都可以通过帮助函数来定义吗?

是的,我包括私有方法。如果我看到一个类的类像一个简单的公共接口,但是却像无穷无尽的私有方法集,而这些私有方法在诸如find_implor find_detail或用途方面有些定义不明确find_helper,那么我也会以类似的方式畏缩。

我建议的替代方法是具有内部链接的非成员非友函数(声明static或在匿名名称空间内),以帮助您实现类,至少比“帮助实现其他功能”更通用。我可以在这里引用C ++“编码标准”中的Herb Sutter,以了解为什么从一般的SE角度来看,它更可取:

避免会员费:在可能的情况下,更希望使职能成为非成员非朋友。[...]非成员非友函数通过最小化依赖关系来改善封装:函数的主体不能依赖于该类的非公共成员(请参阅条款11)。它们还打破了整体类,以释放可分离的功能,从而进一步减少了耦合(请参见条款33)。

您也可以从缩小可变范围的基本原理上理解他所说的“会员费”。如果您想到一个最极端的例子,一个上帝对象具有运行整个程序所需的所有代码,那么偏爱可以访问所有内部组件的此类“帮助程序”(函数,无论是成员函数还是朋友)(私有变量)基本上使这些变量的问题不比全局变量少。在这个最极端的示例中,您将难以正确地管理状态和线程安全以及维护使用全局变量获得的不变量。当然,大多数真实的例子都希望不会接近这个极端,但是信息隐藏的作用仅在于它限制了所访问信息的范围。

现在,萨特(Sutter)在这里已经给出了很好的解释,但我还要补充一点,即在设计功能方面,去耦趋向于像心理上的改善一样促进(至少如果您的大脑像我的那样工作)。当您开始设计只能访问相关参数的,无法访问该类中所有内容的函数时,或者如果您将类的实例作为参数传递给该类的实例(仅是其公共成员),它将倾向于促进有利于设计的思维方式这些功能在去耦和提高封装性方面比在您可以访问所有内容的情况下更容易设计的目的更明确。

如果我们回到四肢,那么充斥着全局变量的代码库并不会完全诱使开发人员以明确且有目的的方式设计功能。很快,您可以在一个函数中访问的信息越多,我们中的更多人就面临着降低其通用性并降低其清晰度的诱惑,从而倾向于访问我们拥有的所有这些额外信息,而不是接受该函数的更具体和相关的参数缩小其进入国家的范围并扩大其适用性并提高其意图的清晰度。这适用于成员函数或朋友(尽管通常程度较小)。


1
感谢您的输入!不过,我不完全理解您从哪里来的这部分内容:“当我在代码中看到很多“帮助者”时,有时我也会有些畏缩。” -不是所有的私有成员函数都可以通过帮助函数来定义吗?总体上,这似乎与私有成员函数有关。
罗伯特·弗雷泽

1
啊,内层阶级根本不需要“朋友”,因此完全避免使用“朋友”关键字
Robert Fraser

“不是所有的私有成员函数都定义了辅助函数吗?这似乎在总体上与私有成员函数有关。” 这不是最大的事情。我以前认为,对于非平凡的类实现,您要么具有多个私有函数,要么具有可以同时访问所有类成员的助手,这在实际中是必需的。但是我看了一些诸如Linus Torvalds,John Carmack之类的伟人的风格,尽管前者使用C语言编写代码,但是当他编码一个对象的类比等效物时,他却设法与Carmack一起对它们进行编码。
Dragon Energy

当然,我认为源文件中的帮助程序比包含大量外部头文件的大量头文件更可取,因为它使用了大量私有函数来帮助实现该类。但是,在研究了上述和其他样式之后,我意识到通常可以编写比需要访问一个类的所有内部成员甚至只实现一个类的类型更泛泛的功能,很好地命名该功能并将其传递给它通常需要工作的特定成员确实节省了更多时间
Dragon Energy

[...]比实际需要的要多,从而产生了更清晰的整体实现,以后更易于操作。这就像为访问您所有内容的“完全匹配”编写“辅助谓词”一样PredicateList,通常只需将谓词列表中的一两个成员传递给一个稍微更通用的函数,而该函数不需要访问的每个私有成员PredicateList,并且往往往往也会为该内部函数产生一个更清晰,更笼统的名称和目的,以及“后见代码重用”的更多机会。
Dragon Energy
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.