如何优雅地解决获取器(getter)的重复问题,包括常量和非常量版本?

159
不是很讨厌当你有的时候吗
class Foobar {
public:
    Something& getSomething(int index) {
        // big, non-trivial chunk of code...
        return something;
    }

    const Something& getSomething(int index) const {
        // big, non-trivial chunk of code...
        return something;
    }
}

我们无法在其中一种方法中实现另一种方法,因为你不能从const版本调用非const版本(编译器错误)。 需要使用转换来从非const版本调用const版本。
如果没有真正优雅的解决方案,那么最接近的解决方案是什么?

5
我认为你需要说明大段代码可能是什么,并至少展示为什么它不能放在一个单独的函数中。 - James Hopkin
模板库明确为某些类型创建了“constType”。这避免了其他方法提到的所有问题(需要一点纪律)。 - CoffeDeveloper
22
重新提出这个问题非常合理。建议的重复问题来自2008年,因此我们现在有了两个新标准(c++11|14),这些标准可能提供比建议的重复问题中提供的更优雅的解决方案。特别是由于建议的const_cast是一个糟糕的解决方案!需要注意的是,这个问题要求优雅的解决方案,而建议的重复问题中并没有要求,也没有给出这样的解决方案。 - Herbert
8个回答

157

我记得在Effective C++的书中提到过,实现非const版本的方法是通过从另一个函数中移除const。

这样做并不太美观,但是是安全的。因为调用它的成员函数是非const的,所以对象本身也是非const的,从中移除const是被允许的。

class Foo
{
public:
    const int& get() const
    {
        //non-trivial work
        return foo;
    }

    int& get()
    {
        return const_cast<int&>(const_cast<const Foo*>(this)->get());
    }
};

15
只要foo不是(并且永远不会成为)一个const成员,那么它是安全的。如果是const成员,则应重新实现get的非const版本(或者重新考虑是否将foo设置为const),但您实际上未能注意到这个问题,因为所有东西都能够编译通过。 - Steve Jessop
7
“a const member” 指的是一个常量成员。或者一个从另一个函数调用返回的const值。基本上,编译器认为它作为const返回,但“实际上”由于强制转换,它被返回为非const。因此,在“this”为非const的情况下,它必须真正是非const的,而编译器无法帮助您执行此操作。 - Steve Jessop
26
static_cast<const Foo*>(this) 应该改为 const_cast<const Foo*>(this) - ildjarn
13
由于调用该函数的成员函数是非常量,因此对象本身是非常量,并且强制转换掉const是被允许的。读了这个陈述很多次后,我终于明白为什么这种方法是安全和正确的。 - Boinst
5
当然,这个想法是"//non-trivial work"显著长于非const方法中的一行代码。否则,这个想法就没有意义了。 - CAdaker
显示剩余15条评论

32

这样怎么样:

template<typename IN, typename OUT>
OUT BigChunk(IN self, int index) {
    // big, non-trivial chunk of code...
    return something;
}

struct FooBar {
    Something &getSomething(int index) {
        return BigChunk<FooBar*, Something&>(this,index);
    }

    const Something &getSomething(int index) const {
        return BigChunk<const FooBar*, const Something&>(this,index);
    }
};

很明显,您仍然会有目标代码重复,但没有源代码重复。与const_cast方法不同的是,编译器将检查方法的两个版本的常量正确性。

您可能需要声明BigChunk的两个有趣的实例为该类的友元。这是使用友元的好处,因为友元函数隐藏在接近被授权者的地方,所以没有无约束的耦合风险(噢!)。但我现在不会尝试语法。请随意添加。

很可能BigChunk需要解除引用self,在这种情况下,上面的定义顺序将无法很好地工作,并且需要一些前向声明来解决它。

此外,为了避免某些人在头文件中找到BigChunk并决定实例化和调用它,即使它在道义上是私有的,您可以将整个内容移动到FooBar的cpp文件中。在匿名命名空间中。具有内部链接性。并带有“当心豹子”的标志。


你能从一个声明为const的函数中调用BigChunk吗?我认为你不能,因为BigChunk没有被声明为const。这是C++模板的一个主要限制。 - HelloGoodbye
1
我想唯一的问题是,除非您有getter/setter方法来访问这些变量,否则您必须将要在函数中使用的所有私有成员变量作为参数发送到该函数,然后可能会出现重复参数类型的问题。您甚至可能需要为这些参数的类型提供模板,因为它们可能需要以const/non-const版本给出。如果您意外使用了不正确的类型,则可能会发生隐式转换为另一种不想使用的数据类型,而没有注意到这一点。 - HelloGoodbye
1
另一方面,您可以将函数声明为const成员函数,仍然为其提供this指针,然后您将自动能够通过this指针访问所有私有成员。但是,这样函数将成为另一个符号,将在包括头文件的所有翻译单元中可见,并且还必须由链接器处理,这反过来会稍微增加链接时间。我想总是有一些陷阱,对吧? :) - HelloGoodbye
@HelloGoodbye 这种想法太牵强了,除非你处于极度受限的环境。编码对构建链来说是一种压力,我们应该关心构建链是否会罢工或其他问题吗?但我仍然同意,意识到成本是积极的,任何成本都是如此。 - v.oddou
1
@HelloGoodbye,与其传递私有成员或创建getter/setter,您可以将函数声明为友元,因为它在技术上是类的一部分。 您可以将模板函数移动到cpp文件中并显式初始化它以解决该问题。当然,这需要进一步的代码。 - Jimmy T.
显示剩余2条评论

8
我会将const转换为非const(第二个选项)。

2
它可以运行,但我认为这并不是非常优雅的解决方案... - Benoît
@Benoît:不是优雅,而是务实。 - Johann Gerell
8
我认为这是const_cast的少数几个有效用途之一——它绝对比代码复制或跳过各种困难要好。 - James Hopkin
我并不反对那个观点 :) - Benoît
2
如果something被声明为const,那么你将会遇到一个问题,但由于程序仍然可以编译,你可能不会注意到它。 - HelloGoodbye

4

const引用对象是有意义的(你对该对象进行了只读访问的限制),但如果需要允许非const引用,则最好将成员设置为公共。

我认为这是Scott Meyers(《Effective C++》作者)的建议。


1
公共成员?不太面向对象... - Michael
我可能会撤回这个说法,因为我没有读到关于拥有“非平凡代码”的部分,这使得那些函数不是直接的getter。 - swongu
1
实际上,当你考虑它时。如果没有被滥用,公共成员是这个问题最简单、最直接的答案。如果任何人可以通过使用适当的 getter/setter 来操纵它们,那么实际上它就是一个公共成员。也许将其设置为 public 甚至会更好,因为它告诉客户端,“我是一个公共成员,你可以对我做任何想做的事情”。 这比实现两种类型的 getter 要更加直截了当。 - Michael
2
“你最好将成员公开化。”不行,因为getter中有一个“大而复杂的代码块”。这个getter不仅仅是暴露一个成员,它还在做重要的工作。当然,是否将结果公开为非const也是另一个问题…… - Steve Jessop

0

尝试通过重构代码来消除getter。如果只有很少的其他事物需要该Something,则使用友元函数或类。

通常,Getter和Setter会破坏封装性,因为数据暴露给了世界。使用友元只将数据暴露给少数人,因此提供更好的封装性。

当然,这并非总是可能的,因此您可能会被迫使用getter。至少,大多数或所有“非平凡代码块”应位于一个或多个私有函数中,由两个getter调用。


14
什么?获取器和设置器会破坏封装性吗?而友元不会?如果类的内部结构(私有部分)发生变化,变量被重命名或删除了怎么办?难道你要重写所有的友元吗?我给你点-1分... - Paulius
7
是的,朋友类的使用确实不太常见。许多 C++ 开发人员认为它们会破坏封装性,但实际上(用 Herb Sutter 的话来说),它们可以增强封装性。 - Johann Gerell
6
Paulius:假设你有两个类彼此使用数据。最好的方式是什么来允许访问那些数据?使用公共getter和setter还是将它们定义为友元?通过公共getter和setter,任何代码都可以使用这些getter和setter。如果使用友元,则只有友元才能访问数据。谁可以看到和/或修改数据的范围大大减少,因此,是的,友元可以比getter和setter提供更好的封装。我真的不知道为什么人们在提到友元时会感到恐慌。友元是好的。 - markh44
8
当班级之间关系良好时,朋友是有益的。如果班级之间不太熟悉,或者另一个班级想要访问数据,你只需添加另一个朋友,那么朋友就会变得很糟糕。C++并不像Facebook,类不应该响应朋友请求。如果这些类没有一起设计,我建议不要使用friend。一旦排除了这个问题,朋友还是很有用的 :-) - Steve Jessop
3
当然,我同意,并且应该在我的回答中明确指出参与友谊的类必须非常密切相关。这不应该仅仅为了方便而使用(同样也不应该使用getter和setter)。 - markh44
显示剩余8条评论

0
“const”这个概念是有其存在的意义的。对我来说,它建立了一个非常重要的契约,基于这个契约,程序的进一步指令被编写。但你可以按照以下方式做:
1. 使你的成员变量“mutable”; 2. 将“getters”设为const; 3. 返回非const引用。
这样,如果你需要在使用getter时保持const功能,就可以在LHS上使用const引用(危险)。但现在责任在程序员身上,需要维护类不变式。
正如之前在SO中所说的,去除原始定义的const对象的const属性并使用它是一种未定义行为。因此,我不会使用强制转换。同时,将非const对象设置为const,然后再次去除const属性也不太好看。
另一个编码指南是:
1. 如果一个成员变量需要在类外部修改,总是通过非const成员函数返回指向它的指针。 2. 没有成员函数可以返回非const引用。只有const成员函数可以返回const引用。

这样可以保持整个代码库的一致性,调用者可以清楚地看到哪些调用可以修改成员变量。


0
为什么不将常用代码提取到一个单独的私有函数中,然后让其他两个函数调用它呢?

10
问题在于,如果私有函数是非const的,则const版本无法调用它。反之,如果私有函数是const的,则不能返回Something&(假设返回值是Foo的成员)。因此,在Something& getSomething(int index)const Something& getSomething(int index) const两种情况下,无法同时调用相同的私有方法。 - sasquires

-3
我敢建议使用预处理器:
#define ConstFunc(type_and_name, params, body) \
    const type_and_name params const body \
    type_and_name params body

class Something
{
};

class Foobar {
private:
    Something something;

public:
    #define getSomethingParams \
    ( \
        int index \
    )

    #define getSomethingBody \
    { \
        return something; \
    }

    ConstFunc(Something & getSomething, getSomethingParams, getSomethingBody)
};

12
我觉得这看起来像是混淆。 - Keith Pinson
3
Stroustrup 反对使用宏。 - outmind

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