简体   繁体   中英

Do non-member non-friend functions really increase encapsulation?

I'm currently readin Scott Meyers's Effective C++ book but I just can't get my head around the 23rd Item. He says:

Prefer non-member non-friend functions to member functions. Doing so increases encapsulation, packaging flexibility, and functional extensibility.

While I can see the point of adding external functions outside the class I don't see advatage of it. He talks about these as they're increasing the encapsulation. Well yeah, that's right since a non-member non-friend function won't have access to any member variables declared within the class as private ones. But, and here's what I just can't get around. Having member functions that allow object control is somewhat essential - what could be done with a POD where all data members are public? I honestly can't see any practical usage there. Havining said that, even if we have non-member non-friend functions, encapsulation won't change as we still need !!public!! MEMBER functions to interact with our object.

So why would I - or anyone else for that matter - prefer non-member non-friend functions over member functions? Sure we can write wrappers over already existing member functions which possibly groups them in logical order but that's all. I can't see any reuduced encapsulation in here.

Meyers gives his reasoning in this article . Here's an extract:

We've now seen that a reasonable way to gauge the amount of encapsulation in a class is to count the number of functions that might be broken if the class's implementation changes. That being the case, it becomes clear that a class with n member functions is more encapsulated than a class with n+1 member functions. And that observation is what justifies my argument for preferring non-member non-friend functions to member functions: if a function f could be implemented as a member function or as a non-friend non-member function, making it a member would decrease encapsulation, while making it a non-member wouldn't.

Meyers does not say avoid member functions. He says that functions should not be members (or friends) unless they need to be . Obviously there need to be some functions which can access the private members of a class otherwise how could any other code interact with the class, right?

But every function which can access the private members of a class is coupled to the private implementation details of that class . The functions which should be members (or friends) are the ones which can only be efficiently implemented by accessing the private details. These are the primitive functions of a class. Non-primitive functions are those which can be efficiently implemented on top of the primitive ones. Making non-primitive functions members (or friends) increases the amount of code which is coupled to the private details.

Also, in writing a function which is able to modify the private members of an object, more care must be taken in order to preserve the class invariants.

Just a little example:

  1. std::list has sort member function, because it benefits from list element's natural moving ability.
  2. But if you cannot get any advantage from internal structure knowledge, there is a general solution — std::sort free function.

I'm going to answer the OP's question of "why would anyone prefer non-member non-friend functions over member functions?" with this simplistic example. Consider an application that generates graphical simulations from geospatial data. The data is ingested in a representation like what you'd expect to see on a compass (in degrees, winding clockwise, where 0 points North/positive on the y-axis). When you pass the direction information to your renderer, it might expect it in a representation like what you're used to from trig (in radians, winding counter-clockwise, where 0 points right/positive on the x-axis).

Since both representations of a direction can be stored as a float, you write a pair of boxed primitives to enforce some type-safety (so you don't accidentally pass an azimuth to a rendering call that expects an angle). To convert between the two representations, you write a member function on Azimuth called AsAngle(), and you write a member function on Angle called AsAzimuth().

class Angle
{
    public:
        float GetValue() const;
        Azimuth AsAzimuth() const;

    private:
        float m_Value;
};

class Azimuth
{
    public:
        float GetValue() const;
        Angle AsAngle() const;

    private:
        float m_Value;
};

The first breakdown of encapsulation here is that now Angle and Azimuth have a dependency on each other's type. You'd need to forward declare one in the other's header, and #include it in the source file so it can construct the other in the conversion function. You could reduce this dependency by having the conversion functions return floats instead of objects of the other class, but that doesn't fully remove logical dependencies on each other, because the next breakdown of encapsulation is that both classes must also know inner details about the other.

If you were to later switch to a renderer that expects angles in degrees instead of radians, you'd change your Angle class for this different representation. However, even though the only change is in the details of what an Angle is, an entirely separate class, Azimuth, now must also change, or else it will continue to return angles in radians instead of degrees. If you update the AsAzimuth() member of Angle but forget to update the AsAngle() member of Azimuth, you could end up with rendering that looks wrong while you scratch your head looking over your changes to Angle for errors when there are none.

Azimuth should not have to care about the inner details of Angle, but it has to when you implement the conversion routine as member functions. If you wrote the conversion as a non-member function, neither class need care about the details of the other anymore – the concern of how to convert between the two representations is now fully encapsulated within a separate function.

If you don't like the idea of having a global function, or some dumping ground for random functions in some kind of utilities namespace, you could improve this design by creating a new Direction class that further encapsulates the details of how direction is stored and converted. It could store the direction however it comes in from the hardware that collects the geospatial data, let's say as an azimuth stored in a float, and have member functions that return it in whatever representation users of the class want, relying solely on visual cues if you do something wrong (such as calling graphicalThingy.SetAngle(direction.AsAzimuth())). But if you don't want to sacrifice the type-safety of the boxed primitives, you could still use the previous two Angle and Azimuth classes, and implement the conversion as a member of Direction. It's still a non-member non-friend function of Angle and Azimuth, it takes in the information it needs from them through their now-smaller public interface using the GetValue() call so it has no access to any of their other private members, it's located in an appropriate place to keep such functions (the Direction class), and neither Angle nor Azimuth needs to care about the details of the other, nor do they have a dependency on each other anymore.

class Direction
{
    public:
        Angle AsAngle() const
        {
            return Angle(Convert(m_Azimuth.GetValue());
        }
        Azimuth AsAzimuth() const
        {
            return m_Azimuth.GetValue();
        }

    private:
        float Convert(const float) const
        {
            ...conversion stuffs here...
        }
        Azimuth m_OriginalAzimuth;
};

In this example, the conversion could be written as a member function, and it does require a piece of private data from the class it's used with. However, there is absolutely no reason to prefer a member function over non-member non-friend function, as the non-member function improves encapsulation.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM