Designs and Declarations, Notes(4), Effective C++
Designs and Declarations
(I read a Chinese version of the book, any translation problem plz point out.
Make interfaces easy to use correctly and hard to use incorrectly
- introduce a new type.
Data d(Month(9), Day(4), Year(2017));
- restrict some invalid operations on types.
addconst
is a useful way.
if(a * b = c) // ...
// It will avoid the problem above.
const Type operator*(const Type& a, const Type& b);
- predefine all the valid values, if we want type safety.
class Month { public: static Month Jan() { return Month(1); } static Month Feb() { return Month(2); } // ... static Month Dec() { return Month(12); } // ... private: explicit Month(int m); // ... };
- make interfaces be compatible with built-in types, aka. providing the interfaces of consistent behaviors.
- force users to use smart pointers. e.g., factory function returns a smart pointer.
std::shared_ptr<Investment> createInvestment();
Treat class design as type design
Questions to think:
- create and destroy
- initialize and assign
- pass by value -> copy constructor
- valid values -> invariants(约束)
- inheritance
- conversion
- functions and operators
- access specifiers
- undeclared interface ???
- generalization -> templates
- really need a new type? maybe some non-member functions or templates achieve the goal.
Prefer pass-by-reference-to-const to pass-by-value
- avoid any construction and destructions, more efficient.
- avoid the slicing problem brought by derived class upcast to base class.
- pass-by-value is proper for built-in types, e.g., STL iterators and functors.
Don’t try to return a reference when you must return an object
try the one that behave correctly, and throw the responsibility to the compilers.
Prefer non-member, non-friend functions to member functions
a natural way, let tool function be a non-member function and put inside the same namespace where the classes it operates is.
then, for future expansion of more tool functions, sperate them in new headers but in the same namespaces.
increase encapsulation, packaging flexibility, and function expansibility.
// webbrowser.h
// Main functionalities of webbrowser.
namespace WebBrowserStuff {
class WebBrowser {
public:
// ...
void clearCache();
void clearHistory();
void removeCookies();
// ...
};
void clearBrowser(WebBrowser& wb) {
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
}
// webbrowser_bookmarks.h
namespace WebBrowserStuff {
// Some tool functions related to bookmarks.
// ...
}
// webbrowser_cookies.h
namespace WebBrowserStuff {
// Some tool functions related to cookies.
// ...
}
Declare non-member functions when type conversions should apply to all parameters
- The parameter can be a participant of implicit type conversion, only when the parameter is in parameter list.
- friend should be avoided when it can be.
Observation: non-member functions is the opposite of member ones, not the friend functions.
Consider support for a non-throwing swap
for pointer to implementation (pimpl), more efficient way is to do:
- provide a public member
swap
function, this function should never throw, because it is the insurance for exception safety.
moreover, the default swap is used for built-in types and the built-in types never throws, we should keep consistent. - provide a non-member
swap
which calls the memberswap
in the namespace where your class or template is. - provide a
std::swap
total template specilization, if you’re trying to design a class not a class template.class Widget { public: void swap(Widget& other) { using std::swap; swap(pImpl, other.pImpl); } }; namespace std { template<> // Total template specilization void swap<Widget>(Widget& a, Widget& b) { a.swap(b); } }
- we are not allowed to change anything in namespace
std
, but we can create specialization.
it is a UB, if you insist on doing so. CPP points out, we can only paritially specialized class templates, function templates is not allowed. so provide non-member
swap
for function templates.namespace WidgetStuff { template<typename T> class Widget { ... }; template<typename T> // Non-member function void swap(Widget<T>& a, Widget<T>& b) { a.swap(b); } }
- when calling
swap
, please ensure ausing std::swap
, then callswap
directly without any namespace specifier.
let the compiler to find a proper one.
CPP name lookup rules (argument-dependent lookup or Koenig lookup rule):
try to find a specificswap
ofT
in global scope or the namespace whereT
is, then the generic one (std::swap
).template<typename T> void doSomething(T& obj1, T& obj2) { using std::swap; // ... swap(obj1, obj2); // ... }