Rationale
- maintainability: no need to change further as these classes evolve
- clarity for developers reading the code and this description
- performance in some cases
Excerpts from Scott Meyers’ “Effective C++, Third Edition” (2005), Item 20, pages 86-94, and “Effective Modern C++” (2015), Item 41, pages 281-292:
-
Avoid passing objects of user-defined types by value.
-
One might conclude that all small types are good candidates for pass-by-value, even if they’re user-defined. However, just because an object is small doesn’t mean that calling its copy constructor is inexpensive. Many objects–most STL containers among them–contain little more than a pointer, but copying such objects entails copying everything they point to. That can be very expensive.
-
Even when small objects have inexpensive copy constructors, there can be performance issues. Some compilers treat built-in and user-defined types differently, even if they have the same underlying representation. When that type of thing happens, you can be better off passing such objects by reference, because compilers will certainly put pointers (the implementation of references) into registers.
-
Another reason why small user-defined types are not necessarily good pass-by-value candidates is that, being user-defined, their size is subject to change. A type that’s small now may be bigger in the future if its internal implementation changes. Things can even change when switching to a different C++ implementation.
-
In general, the only types for which you can reasonably assume that pass-by-value is inexpensive are built-in types and STL iterator and function object types. For everything else, prefer pass-by-reference-to-const over pass-by-value.
-
Exception: possibly consider pass-by-value of user-defined types for copyable parameters that are cheap to move, if they are always copied anyway in their implementation.
-
When there are chains of function calls, each of which passes-by-value, the cost for the entire chain of calls accumulates and may not be something you can tolerate. With by-reference passing, chains of calls don’t incur this kind of accumulated overhead.
-
The most practical approach may be to adopt a “guilty until proven innocent” policy for pass-by-value of user-defined types.