How Non-Member Functions Improve Encapsulation

How Non-Member Functions Improve Encapsulation

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.

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

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.

Non-friend, non-member functions increase encapsulation?

In theory, you sort of could do this, but you really don't want to. Let's consider why you don't want to do this (for the moment, in the original context--C++98/03, and ignoring the additions in C++11 and newer).

First of all, it would mean that essentially all classes have to be written to act as base classes--but for some classes, that's just a lousy idea, and may even run directly contrary to the basic intent (e.g., something intended to implement the Flyweight pattern).

Second, it would render most inheritance meaningless. For an obvious example, many classes in C++ support I/O. As it stands now, the idiomatic way to do that is to overload operator<< and operator>> as free functions. Right now, the intent of an iostream is to represent something that's at least vaguely file-like--something into which we can write data, and/or out of which we can read data. If we supported I/O via inheritance, it would also mean anything that can be read from/written to anything vaguely file-like.

This simply makes no sense at all. An iostream represents something at least vaguely file-like, not all the kinds of objects you might want to read from or write to a file.

Worse, it would render nearly all the compiler's type checking nearly meaningless. Just for example, writing a distance object into a person object makes no sense--but if they both support I/O by being derived from iostream, then the compiler wouldn't have a way to sort that out from one that really did make sense.

Unfortunately, that's just the tip of the iceberg. When you inherit from a base class, you inherit the limitations of that base class. For example, if you're using a base class that doesn't support copy assignment or copy construction, objects of the derived class won't/can't either.

Continuing the previous example, that would mean if you want to do I/O on an object, you can't support copy construction or copy assignment for that type of object.

That, in turn, means that objects that support I/O would be disjoint from objects that support being put in collections (i.e., collections require capabilities that are prohibited by iostreams).

Bottom line: we almost immediately end up with a thoroughly unmanageable mess, where none of our inheritance would any longer make any real sense at all and the compiler's type checking would be rendered almost completely useless.

Large scale usage of Meyer's advice to prefer Non-member,non-friend functions?

OpenCV library does this. They have a cv::Mat class that presents a 3D matrix (or images). Then they have all the other functions in the cv namespace.

OpenCV library is huge and is widely regarded in its field.

When should I prefer non-member non-friend functions to member functions?

More generally, what are the rules of when to use which?

Here is what Scott Meyer's rules are (source):

Scott has an interesting article in print which advocates
that non-member non-friend functions improve encapsulation
for classes. He uses the following algorithm to determine
where a function f gets placed:

if (f needs to be virtual)
make f a member function of C;
else if (f is operator>> or operator<<)
{
make f a non-member function;
if (f needs access to non-public members of C)
make f a friend of C;
}
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;
}
else if (f can be implemented via C's public interface)
make f a non-member function;
else
make f a member function of C;

His definition of encapsulation involves the number
of functions which are impacted when private data
members are changed.

Which pretty much sums it all up, and it is quite reasonable as well, in my opinion.

Effective C++ Item 23 Prefer non-member non-friend functions to member functions

Access to the book is by no mean necessary.

The issues we are dealing here are Dependency and Reuse.

In a well-designed software, you try to isolate items from one another so as to reduce Dependencies, because Dependencies are a hurdle to overcome when change is necessary.

In a well-designed software, you apply the DRY principle (Don't Repeat Yourself) because when a change is necessary, it's painful and error-prone to have to repeat it in a dozen different places.

The "classic" OO mindset is increasingly bad at handling dependencies. By having lots and lots of methods depending directly on the internals of the class, the slightest change implies a whole rewrite. It need not be so.

In C++, the STL (not the whole standard library), has been designed with the explicit goals of:

  • cutting dependencies
  • allowing reuse

Therefore, the Containers expose well-defined interfaces that hide their internal representations but still offer sufficient access to the information they encapsulate so that Algorithms may be executed on them. All modifications are made through the container interface so that the invariants are guaranteed.

For example, if you think about the requirements of the sort algorithm. For the implementation used (in general) by the STL, it requires (from the container):

  • efficient access to an item at a given index: Random Access
  • the ability to swap two items: not Associative

Thus, any container that provides Random Access and is not Associative is (in theory) suitable to be sorted efficiently by (say) a Quick Sort algorithm.

What are the Containers in C++ that satisfy this ?

  • the basic C-array
  • deque
  • vector

And any container that you may write if you pay attention to these details.

It would be wasteful, wouldn't it, to rewrite (copy/paste/tweak) sort for each of those ?

Note, for example, that there is a std::list::sort method. Why ? Because std::list does not offer random access (informally myList[4] does not work), thus the sort from algorithm is not suitable.

Using non-member non-friend functions instead of member functions: disadvantages?

You could provide a tuple-like view of Metadata, and have the conversion functions instantiate a std::index_sequence to populate the result;

// file Metadata.h
class Metadata
{
// Getters
std::string GetDescription() const;
std::string GetTimeStamp() const;
float GetExposureTimeInMilliSeconds() const;

template<size_t I> static const char * name();

// Setters
// ...
private:
std::string m_description;
std::string m_timeStamp;
float m_exposureTimeInMilliSeconds;

// Added later with associated getters/setters:
// std::string m_location;
// std::string m_nameOfPersonWhoTookThePicture;
};

namespace std
{
template<> class tuple_size<Metadata> : public std::integral_constant<std::size_t, 3> {}; // later 5

template<> class tuple_element<0, Metadata>{ using type = std::string; };
template<> class tuple_element<1, Metadata>{ using type = std::string; };
template<> class tuple_element<2, Metadata>{ using type = float; };
/* Later add
template<> class tuple_element<3, Metadata>{ using type = std::string; };
template<> class tuple_element<4, Metadata>{ using type = std::string; };
*/
}

template<size_t I> std::tuple_element_t<I, Metadata> get(const Metadata & meta);
template<> std::string get<0>(const Metadata & meta) { return meta.GetDescription(); }
template<> std::string get<1>(const Metadata & meta) { return meta.GetTimeStamp(); }
template<> float get<2>(const Metadata & meta) { return meta.GetExposureTimeInMilliSeconds(); }
/* Later add
template<> std::string get<3>(const Metadata & meta) { return meta.GetLocation(); }
template<> std::string get<4>(const Metadata & meta) { return meta.GetPhotographerName(); }
*/

template<> const char * Metadata::name<0>() { return "Description"; }
template<> const char * Metadata::name<1>() { return "Time Stamp"; }
template<> const char * Metadata::name<2>() { return "Exposure Time"; }
/* Later add
template<> const char * Metadata::name<3>() { return "Location"; }
template<> const char * Metadata::name<2>() { return "PhotographerName"; }
*/

The conversion function then doesn't change when you add members

// File UtilityFunctions.h
namespace UtilityFunctions
{
namespace detail
{
template<size_t... Is>
ENVIMetadata ConvertMetadataToENVIMetadata(const Metadata &i_metadata, std::index_sequence<Is...>)
{
ENVIMetadata envi;
envi.AddMetadata<std::tuple_element_t<Is, Metadata>>(Metadata::name<Is>(), get<Is>(i_metadata))...;
return envi;
}
}

ENVIMetadata ConvertMetadataToENVIMetadata(const Metadata &i_metadata)\
{
return detail::ConvertMetadataToENVIMetadata(i_metadata, std::make_index_sequence<std::tuple_size_v<Metadata>>{})
}
}

Preferring non-member non-friend functions to member functions

Usually, you would put them in the associated namespace. This serves (somewhat) the same function as extension methods in C#.

The thing is that in C#, if you want to make some static functions, they have to be in a class, which is ridiculous because there's no OO going on at all- e.g., the Math class. In C++ you can just use the right tool for this job- a namespace.

Why does the C++ Core Guidelines recommends the prefer independent functions instead of class members?

Member functions have access to private members of a class. This means that they can use the internal representation of the class in their logic.

Non-member functions only have access to public members of a class. This means that they can only use the publicly exposed interface of the class, thus improving encapsulation.

C++ Friend Functions Improve Encapsulation?

I'd say that a friend function is simply an extension of the public interface of the class, which uses a slightly different syntax and allows implicit conversions on all of its parameters (whereas member functions don't do that on their first/implied parameter).

In other words, the author of the class which grants friendship should be the one in control of the friend function. If you just declare a friend function in your class and allow clients to define that function, then certainly hell breaks loose (and program breaks down). But that's not what friend functions are for.



Related Topics



Leave a reply



Submit