把非成员函数放在类内是一种好的实践吗?

3

我正在阅读这样的代码:

class Member
{
public:
    friend std::istream& operator>>(std::istream& in, Member& m)
    {
        in >> m.name >> m.bYear >> m.bMonth;
        return in;
    }

    friend std::ostream& operator<<(std::ostream& out,const Member& m)
    {
        out << m.name << " " << m.bYear << "." << m.bMonth;
        return out;
    }

private:
    std::string name;
    int year;
    int month;
};

我以前从未见过这种写法。在类体内使用 friend 定义非成员函数是一种好的实践吗?有什么优缺点吗?


1
这是我经常自己做的事情(对于微不足道的内联 I/O),因为它表达了函数与类之间的亲密联系。我没有看到任何真正的负面影响。无论如何,你必须在类定义中声明这些函数。 - Galik
1
如果类和它的友元是模板,那么尝试在类外定义友元会变得非常混乱。https://dev59.com/X3LYa4cB1Zd3GeqPaq4L - Mike Seymour
2
@Theolodis:不是重复问题,friend的使用并不在讨论范围内。 - Deduplicator
2
@JamesKanze:也许我们对“混乱”的定义不同。在大多数情况下,必须先声明类模板,然后再声明函数模板,这确实让人感到混乱。 - Mike Seymour
1
@JamesKanze:我的(有点离题)评论是,如果它们是模板,那么由于需要额外的声明,情况会变得混乱,就像我链接到的问题所示。如果它们不是模板,那么你是对的,将接口与实现分开并没有更加混乱(也许根据个人喜好而言,更可取)。如果我的评论超出了问题的范围,对此我表示抱歉,可能给你带来了困惑。 - Mike Seymour
显示剩余3条评论
3个回答

5
在类体内使用友元定义非成员函数是一个中性的做法。
优点:
- 运算符可以在类的作用域内引用类成员(嵌套类、typedef、枚举、常量、静态函数等)而无需显式地加上类名前缀。 - 流操作符隐式地被定义为`inline`,方便易用,不需要担心单一定义规则的问题。 - 友元关系使得可以方便地访问所有非公有成员。 - 学习类源代码的人更容易注意到流操作的能力。 - 如果该类是一个模板,则定义一个友元可以省略运算符中的`template <...>`部分,并将实例参数简单地引用为`const Member&`而不是`const Member<...>&`。
缺点:
- 可能需要外部定义函数以便修改实现,只需要重新链接而不需要重新编译客户端代码。 - 授权友情可能并不是必要的,这会降低封装性和可维护性。 - 寻找非成员流操作符的人可能不会想到在类代码中寻找。 - 可以争论它“混乱了”类定义源代码,使得很难理解实际类成员的总体情况。
通常,对于由不同高级库和应用程序使用的低级库来说,清晰分离接口和实现的好处 - 无论是为了管理物理依赖关系(需要重新编译而不仅仅是重新链接)还是为了人类可读性 - 往往会增加,并且对于“私有”支持本地实现(例如,一个匿名命名空间中的一个类,在单个翻译单位中使用,或者更多地说是一个`private`嵌套类)则远远低得多。

记住,类是用于封装(减少依赖关系)的。 - Deduplicator

4
通常情况下,这样做并不是一个好的实践方法;理想情况下,实现应该不在同一个文件中作为类定义。(同时,理想情况下,我们也不需要在头文件中声明私有部分。)然而,有很多正当的例外:
  • 最显而易见的情况是非常简单的帮助类,在这种情况下,没有足够的理由将两个部分分开。如果辅助类在源文件中本地定义而不是在头文件中定义,则尤其如此。

  • 另一种情况是朋友,特别是在模板中。如果我写(即使在模板中)friend void f(MyClass&),那么我已经声明了一个非模板的朋友,并且我必须为每个实例化类型实现一个单独的非模板函数。然而,如果我在类定义中提供内联实现,则编译器将自动为每个使用的类型创建单独的非模板函数。这是定义class中的operator>>和operator <<的非常频繁的动机,因为它们不能成为成员;即使它们不需要访问私有成员,它们通常也会被声明为朋友,以便可以以这种方式定义它们。

最后,如果没有其他函数或运算符的声明,则它们只在类内部可见,或者通过ADL可见。只要函数至少有一个涉及类的参数,这不应该是一个问题。


如果你追求完美,希望使用适当的模块(这意味着根本不需要头文件)。委员会正在努力解决这个问题... - Deduplicator
@Deduplicator 当然可以。但在那之前...(我强烈主张我们需要为模板提供“导出”功能,因为我们没有模块的概念。我也有点反对命名空间,因为它只完成了一半的工作,而我们需要模块。) - James Kanze

1

优点:

如果您将所有或大多数其他函数处理类的私有成员定义在类体内部,则更易于阅读和维护。它使事情保持在一起。

缺点:

在类体中定义的函数出现在每个编译单元中,而不仅仅是编译相应的.cpp文件的一个单元中。


如果该函数应该是内联的,因为性能或者它是模板化的,那么这并不是一个缺点。 - Deduplicator
@JamesKanze:在这个例子中,耦合性已经足够强了,分离定义并没有任何好处,只会增加更多的工作量。当然,如果减少耦合性可能会有所不同。 - Deduplicator
@Deduplicator:“将定义分离出来根本没有任何好处,只会增加更多的工作”,这忽略了内联和外部(以及在实现文件中)定义的重新编译与“仅”重新链接后果之间的常见差异。正如Lakos所说的,“物理”依赖关系/“重新编译防火墙”等等…… - Tony Delroy
@Deduplicator 我不明白你的意思。实现就是实现,对客户端可见性越小越好。(在大型项目中,通常不能更改接口而不进行架构审查,因为这会导致代码出现问题。确保这一点的最简单方法是将接口和实现放在两个不同的文件中,并且除非被授权更改,否则不要签出带有接口的文件。) - James Kanze
好的,应该更像是“耦合足够强大,实现足够简单”。 - Deduplicator
显示剩余2条评论

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