How Non-Member Functions Improve Encapsulation

2020-01-26 09:42发布

问题:

I read Scott Meyers' article on the subject and quite confused about what he is talking about. I have 3 questions here.

Question 1

To explain in detail, assume I am writing a simple vector<T> class with methods like push_back, insert and operator []. If I followed Meyers' algorithm, I would end up with all non-member friend functions. I will have a vector class with few private members and many non-member friend functions. Is this what he is talking about?

Question 2

I am still not understanding how non-member functions improve encapsulation. Consider the code given in Meyers' article.

class Point {
public:
   int getXValue() const; 
   int getYValue() const; 
   void setXValue(int newXValue);
   void setYValue(int newYValue);

private:
  ...                 // whatever...
};

If his algorithm is followed, setXXXX methods should be non-members. My question is how that increases encapsulation? He also says

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.

Until we keep the method signature intact when class implementation changes, no client code is gonna break and it is well encapsulated, right? The same applies for non-member functions as well. So what is the advantage non-member function provides?

Question 3

Quoting his algorithm

else if (f needs type conversions
         on its left-most argument)
   {
   make f a non-member function;
   if (f needs access to non-public
       members of C)
      make f a friend of C;
   }

What he meant by f needs type conversions on its left-most argument? He also says the following in the article.

Furthermore, we now see that the common claim that "friend functions violate encapsulation" is not quite true. Friends don't violate encapsulation, they just decrease it — in exactly the same manner as a member functions.

This and the above algorithm are contradictory, right?

回答1:

Question 1

In this case, following Meyers's algorithm will give you member functions:

  • Do they need to be virtual? No.
  • Are they operator<< or operator>>? No.
  • Do they need type conversions? No.
  • Can they be implemented in terms of the public interface? No.
  • So make them members.

His advice is to only make them friends when they really need to be; to favour non-member non-friends over members over friends.

Question 2

The SetXXXX functions need to access the internal (private) representation of the class, so they can't be non-member non-friends; so, Meyers argues, they should be members rather than friends.

The encapsulation comes about by hiding the details of how the class is implemented; you define a public interface separately from a private implementation. If you then invent a better implementation, you can change it without changing the public interface, and any code using the class will continue to work. So Meyers's "number of functions which might be broken" counts the member and friend functions (which we can easily track down by looking at the class definition), but not any non-member non-friend functions using the class through its public interface.

Question 3

This has been answered.

The important points to take away from Meyers's advice are:

  • Design classes to have clean, stable public interfaces separate from their private implementation;
  • Only make functions members or friends when they really need to access the implementation;
  • Only make functions friends when they can't be members.


回答2:

The meaning f needs type conversions on it left-most arg is as follows:

consider following senario :

Class Integer 
{  
    private: 
       int num;
     public:
        int getNum( return num;)
        Integer(int n = 0) { num = n;}

    Integer(const Integer &rhs)) { num = rhs.num ;}
    Integer operator * (const Integer &rhs)
    {
         return Integer(num * rhs.num);
    }
}


int main()
{
    Integer i1(5);

    Integer i3 = i1 *  3; // valid 

    Integer i3 = 3 * i1 ; // error     
}

In above code i3 = i1 * 3 is equivalent to this->operator*(3) which is valid since 3 is implicitly converted to Integer.

Where as in later i3 = 3 * i1 is equivalent to 3.operator*(i1) , as per rule when u overload operator using member function the invoking object must be of the same class. but here its not that.

To make Integer i3 = 3 * i1 work one can define non-member function as follow :

Integer operator * (const Integer &lhs , const Integer &rhs) // non-member function
    {

         return Integer(lhs.getNum() * rhs.getNum());

    }

I think you will get idea from this example.....



回答3:

Of the four cases he provides for making functions non-members, the closest that your proposed vector methods would come to is this one:

else if (f can be implemented via C's
         public interface)
   make f a non-member function;

But you can't implement methods like push_back, insert or operator[] via a public interface. Those are the public interface. It might be possible to implement push_back in terms of insert, but to to a large degree, what public interface are you going to be using for such methods?

Further the cases for giving friendship to non-member functions are really special cases as I see it, operator<< and operator>>, and type conversions, would both require very accurate, and unfiltered data from the class. These methods are naturally very invasive.

While I'm not a fan of Dr. Dobbs, or any of the claimed "C++ gurus", I think in this case you might be double guessing your own implementation. Scott Meyer's algorithm seems reasonable to me.



回答4:

Take a good look at the STL algorithms. sort, copy, transform etc operate on iterators and aren't member functions.

You're also wrong about his algorithm. The set and get functions can't be implemented with Point's public interface.



回答5:

Question : 2

Scott Meyers has also suggested following thing if u remember :

--> Keep the class interface complete and minimal.

See following scenario :

class Person {

private: string name;

         unsigned int age;

         long salary;

public:

       void setName(string);// assme the implementation

       void setAge(unsigned int); // assme the implementation

       void setSalary(long sal); // assme the implementation

       void setPersonData()
       {
           setName("Scott");
           setAge(25);
           selSalary(50000);
        }
}

here setPersonData() is member function but ultimately what its doing can also be achieved by making it non - member function like this and it will keep interface of class minimal and does not bloat class with plenty of member function unnecessarily .

   void setPersonData(Person &p) 
   {           
       p.setName("Scott");
       p.setAge(25);
       p.selSalary(50000);
    }


回答6:

I suppose the general point is that it is beneficial to always implement things in terms of other things if you can. Implementing functionality as non-friend free functions, ensures that this functionality doesn't break if you change the class representation.

In real life, I guess it might have a problem: you might be able to implement something in terms of the public interface with the current implementation, but if there are changes to the class, this might not be possible any more (and you'll need to start declaring things friends). (E.g when it comes to algorithmic optimization, the free function might benefit from some extra cached data, that shouldn't be exposed to the public.)

So the guideline I'd derive from it: use common sense, but don't be afraid of free functions. They don't make your C++ code less object-oriented.


Another things is probably an interface consisting entirely of getters and setters. This hardly encapsulates anything.

In case of Point in particular, you might get the temptation to store the data as int coords[2] instead, and in this respect the getters and setters might have a meaning (but one might also always consider the ease of use vs ease of implementation).

But if you move on to more complicated classes, they should do something (some core functionality) other than just giving access to their data.


When it comes to vector, some of its methods could have been free functions: assign (in terms of clear + insert), at, back, front (in terms of size + operator[]), empty (in terms of size or begin / end), pop_back (erase + size), push_back (insert + size), end (begin + size), rbegin and rend (begin and end).

But if taken rigorously, this could lead to rather confusing interfaces, e.g

 for (vector<T>::iterator it = v.begin(); it != end(v); ++it)

Furthermore, here one would have to consider the capabilities of other containers. If std::list cannot implement end as a free function, then std::vector shouldn't either (templates need one uniform pattern for iterating over a container).

Again, use common sense.



回答7:

He specifically says "non-member non-friend functions" (emphasis mine). If you would need to make the non-member function a fiend his algorithms says it should be a member function unless it's operator>> or operator<< or needs type conversions on its left-most argument.



回答8:

Until we keep the method signature intact when class implementation changes, no client code is gonna break and it is well encapsulated, right? The same applies for non-member functions as well. So what is the advantage non-member function provides?

Meyers is saying that a class with many methods is less encapsulated than a class with fewer methods, because the implementations of all those internal methods are subject to changing. If any of the methods could have been non-members, that would reduce the number of methods that could be affected by changes internal to the class.

What he meant by f needs type conversions on its left-most argument?

I think he's referring to operators, functions that would have an implicit left-most argument if they were member functions.



标签: c++ function