Effective C++ Item 23 Prefer Non-Member Non-Friend Functions to Member Functions

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.

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.

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.

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.

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.

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>>{})
}
}

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.

Does Scott Meyers's advice to prefer non-member non-friend methods apply to object construction?

One drawback is a bit of inconsistency, which is an esthetic concern.

You're calling a CFromString constructor function rather than invoking a constructor called C. The relationship between them is arbitrary, just through the C prefix in the name.

If you make it a static member function, then you can call it C::FromString so that it belongs to the class.

If this is done all over the place in a large project, some sort of convention would help. Like say, whenever we have a class C, and a non-member constructor function for making C-s, let's always call it CCons, and then always use overloading for different types. Thus, if we have a Widget class, we then call our overloaded family WidgetCons:

Widget WidgetCons(const std::string &s) { /* make it from string */ }
Widget WidgetCons(int i) { /* make it from int */ }

and so on. If this is consistent in our 250,000 line codebase, whenever someone sees any FooCons or BarCons, they know exactly what it is.



Related Topics



Leave a reply



Submit