Effective C++ 55 Things To Improve Your Program
Author of the book is Scott Meyer
Item 1 C++ is a combination of 4 aspect
C
C++ is a super type of C and contains primitive structure and low level constructs that are inherently unsafe. When working with such code there doesn’t exist any templates, exceptions and overloading.
Object Oriented C++
C++ was built on the foundation of class ideas such as [[encapsulation]], [[inheritance]], [[polymorphism]], [[virtual functions]].
Template C++
Templates provide good way to generalize code using the idea similar to generics in java. In C++, this can be used within classes, functions and perform type conversion during runtime to simplify code.
STL
C++ provides standard library with set of container, iterator and algorithms.
Key Take Away
- Effective C++ Programming depends on the parts of C++ you are using
Const, enums over #defines
Constants
Use
class Widget {
private:
static const float PI = 3.14; // simple example
static const double RATIO; // out of line example
enum { SIZE = 5 }; // this could be an option too
int gears[SIZE];
}
// implementation of ratio
Widget::RATIO = 1.5;
Instead of
#define PI 3.14
#define RATIO 1.5
#define SIZE 5
Key Insights
#defineoften pollute the entire compilation and will continue to do so unless#undefined- Some time you might need to implement the constant out of line.
- enum variable reference can’t be accessed
Functions
Use
template<typename T> // because we don't know what T is, we
inline void callWithMax(const T& a, const T& b){ // pass by reference-to-const
f(a > b ? a : b);
}
Instead of
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
CALL_WITH_MAX(++a, b); // a is incremented twice
CALL_WITH_MAX(++a, b+10); // a is incremented once
Key Insight
- Inline function doesn’t pollute the name space unless imported
- Defined macro can lead to inconsistent input if input is complex
Key Take Away
- Use const in object instead of
#defineconstant - For function like macro, prefer inline function to
#define
Use Const when possible
Const Pointer Vs Const Data
char greeting[] = "hello";
char *p = greeting; // non-const pointer,
// non-const data
const char *p = greeting; // non-const pointer,
// const data
char * const p = greeting; // const pointer,
// non-const data
const char * const p = greeting; // const pointer,
// const data
Iterator STL Pattern
std::vector<T>::iterator in STL uses the type T* const which means that the content of the iterator can be modified. For content immutable iterator, use the std::vector<T>::const_iterator .
For example
const std::vector<int>::iterator iter = vec.begin();
// iter acts like a T* const
*iter = 10; // OK, changes what iter points to
++iter; // error! iter is const
std::vector<int>::const_iterator cIter = vec.begin();
//cIter acts like a const T*
*cIter = 10; // error! *cIter is const
++cIter; // fine, changes cIter
Apply const to function return type to avoid potential comparison error
Say if we have the following
const Rational operator*(const Rational& lhs, const Rational& rhs);
the additional const would prevent the following code from compiling successfully
if ((a*b) = c) { // typo should have been ==
...
}
Over load Const Member Function
const member function is where you add const after the function argument specifying that the function would note mutate any non static field or parameter within the function code.
class TextBlock {
const char& operator[](std::size_t position) const {
return text[position];
} // operator[] for const objects
char& operator[](std::size_t position){
return text[position];
} // operator[] fornon-const objects
}
usage might look something like this
TextBlock tb("Hello");
std::cout << tb[0];// calls non-const TextBlock::operator[]
tb[0] = 'x'; // fine
const TextBlock ctb("World");
std::cout << ctb[0]; // calls const TextBlock::operator[]
ctb[0] = 'x'; // error!
Mutable Flags
When you would like some field to be still mutable even in certain situation you marked the function as const member, you can use the mutable flag. Here is an example.
class CTextBlock {
public:
std::size_t CTextBlock::length() const{
if (!cacheValid_){
length_ = computeLength(); // fine
}
return length_;
}
private:
mutable int length_;
bool cacheValid_;
}
Avoid Code Duplication With Const and non Const member function
Sometimes we might have code which are very same across the const and non-const member function which we but we would like to avoid duplication by maintaining two implementation. We can do the following.
class TextBlock {
public:
// const version
const char& operator[](std::size_t position) const {
...
// step 1, 2, 3 and so on
return text[position];
}
// non const version
char& operator[](std::size_t position){
return
// cast away const on op[]'s return type;
const_cast<char&>(
// add const to *this's type;
// call const version of op[]
static_cast<const TextBlock&>(*this)[position]
);
}
};
With this implementation, we know only have one implementation and avoided duplicate code across both side.
Note that the other way will not work because having const version calling non const version could potentially violate the const-ness of as non const version doesn’t have the constraint.
Key Take Away
- Declaring Const help compiler detect error.
constcan be apply to primitives, objects, function parameter, member function, and return type- Compiler enforce bitwise correctness to prevent mutation in function, but passing reference out side technically doesn’t violate this constraint but this allows the caller to still mutate the output.
- When const and non-const has same implementation, use the non-const to call the const version.
Initialize Object
Class Initializer
In most cases, we can initialize the object by using the following syntax.
// h file
class Widget {
public:
Widget(const std::string& name);
private:
std::string name_;
};
// cpp file
Widget::Widget(const std::string& name): name_(name) {
// other logic here
}
Using initialization instead of assignment can allow more optimization to be done by the compiler.
Explicit Initializer
When you pass in empty into the initializer. It will use the default constructor of that particular field.
class Widget {
public:
Widget(): vec_() { // here vector is initialized to default
}
private:
std::vector<int> vec_;
}
Non Deterministic Of Non Local Static Object Initialization
If you have a non local (i.e global static object) like the following
// file 1
class Widget {
...
};
extern Widget w;
and that another static object depends on
class Table {
public:
Table() {
int size = w.size();
}
};
Table t;
The C++ run time does not guarantee that w is available during the initialization of table t. Therefore, we could run into undefined behavior. Alternatively we can do the following.
class Widget {
...
};
Widget& getGlobalWidget(){
static Widget w;
return w;
}
class Table {
Table::Table(){
...
int size = getGlobalWidget().size();
...
}
...
}
Table& getGlobalTable() {
static Table t;
return t;
}
This way it guarantees that global widget is always initialized as it is done in part of the execution flow.
Key Take Away
- Manually initialize object of built in type. Because C++ only sometime initialize them.
- Prefer the use of initialization list to assignment inside the body of the constructor
- Avoid initialization order problem by replacing non local static object with local static object (i.e inside function).
Default class constructor, destructor, and assignment operator
Default generated functions
When you create a class without any explicit constructor, destructor, and assignment operator. C++ will generate a default inline set for you. If we have the following
class Empty {}
It is equivalent to
class Empty {
public:
Empty() { ... }
Empty(const Empty& rhs) { ... } // copy all non static data
~Empty(){ ... }
Empty& operator=(const Empty& rhs) { ... } // copy all non static data
}
Reference field inside a class
When you have a reference field within a class
class Person {
Person(String& name);
private:
String& name_;
}
In this case, when you attempt to assign one person to another, compiler will refused to do so as C++ is unable to reassign references field.
Key Insight
- Compilers may implicitly generate a class’s default constructor, copy constructor, copy assignment operator, and destructor.
Block Generated Function
In order to prevent generated function as well as their usage we could mark the copy, assignment and other generated operator as private and don’t implement them which would result in a linker error when the function is called. Here is a simple example.
class Uncopyable {
protected:
Uncopyable() {}
~Uncopyable() {}
private:
// don't implement this in cpp files
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
}
Key Insight
- To disallow functionality automatically provided by compilers, declare the corresponding member functions private and give no implementations. Using a base class like
Uncopyableis one way to do this.
Declare Destructor Virtual
Make Base Class Virtual
When using a base class with sub classes, make sure to mark the destructor as virtual as this forces the sub class to implement the destructor and properly free its resources as the base class won’t be able to see the children’s resources and can lead to memory leak. Here is an example.
class Car {
public:
Car();
virtual ~Car();
};
Car* c = (Car*) new Limo();
// Without the virtual destructor
// we would only run the car distructor.
// Often lead to memory leak
delete c; // run the Limo destructor
Non Base Class Should Not have virtual destructor
Class with virtual destructor will increase the overall size due to the additional vptr it must also carry along to indicate the virtual function it posses. For non base class that doesn’t get inherited should not use virtual constructor.
Be Careful When Inheriting Classes From Third Party
When you inherit from third party library such as std::string or any other common classes. Be mindful that they may not have virtual destructor and later casting them into their base class can easily lead to memory leak.
Key Insights
- Polymorphic base classes should declare virtual destructors. If a class has any virtual functions, it should have a virtual destructor.
- Classes not designed to be base classes or not designed to be used polymorphically should not declare virtual destructors.
Do not throw exception inside destructor
Exception in destructor can cause memory leak
When destructor of the object throws exception, it can cause other objects that’s within a same set of a collection (e.g vector, list, map) to escape the original control flow which may resulted in other object within the same collection to not properly allocated and create memory leak. However, it may not always be possible to avoid this. Below are three ways to
Handle the exception in destructor by terminating program
In certain situation, it may be best to terminate the program if the exception is severe and is unable to recover. In this case, the best way is to terminate the program. One way to perform this would be to catch the exception and use std::abort() to close the program. Example
DBConn::~DBConn(){
try {
db.close();
}
catch (...) {
// make log entry that the call to close failed;
std::abort();
}
}
Handle exception by swallowing the exception
This is generally a bad idea as it suppress the fact that something failed and can become bigger issue later on. However, in very rare situation, we can suppress the error assuming that the program can continue to execution as normally would. In most cases this is not the case.
DBConn::~DBConn(){
try {
db.close();
}
catch (...) {
// make log entry that the call to close failed;
// continue execution here
}
}
Pass responsibility to clients/caller
Another potential way to handle this situation is to offer the api which could cause the exception in question to be called by the caller. In our db connection example. We could create the following function to let caller handler the exception. Below is an example.
class DBConn {
public: ...
void close() { // new function for client to use
db.close();
closed = true; // flag used by the destructor
}
~DBConn() {
if (!closed) { // note this should be a back up call
try {
// close the connection if the client didn't
db.close();
}
catch (...) { // if closing fails,
// make log entry that call to close failed;
// note that and terminate or swallow
}
}
private:
DBConnection db;
bool closed;
};
Key Insights
- Destructor should never emit exceptions. If function used by destructor throws any exception, the destructor should catch them and decide to terminate the program or swallow them.
- Class should provide another set of wrapper function to help handle the free up and handle any exception during execution by the caller rather than inside the destructor. Leave destructor call as backup case.
Don’t Call Virtual Function Inside Constructor or Destructor
Base class function will be called inside the constructor.
Let’s say we have a base class which contains a set of function to run in the constructor. If we have the derived (child) class override those function inside their implementation, they will still run THE ORIGINAL base class function. This is unlike other OO languages such as java or C# where function overriding occur throughout.
class Car {
Car(){
buildEngine();
}
virtual void buildEngine(){
cout << "click"; // this one would be ran
}
}
class SuperCar: Car {
//...
SuperCar(){
}
virtual void buildEngine() {
// this would not be run
cout << "roff";
}
}
SuperCar sc; // Click
Knowing the this fact, running virtual function in the base class constructor or destructor will result in linker error or worse (unexpected run behavior) if that function exists with implementation within the base class.
Resolve the generalization through dependency injection
One to resolve this is to pass the unique implementation detail such as necessary class information and detail of that particular shared virtual function into the base class constructor and let the base class run the code. For example,
class Car {
Car() {
buildEngine("click")
}
Car(std::string engineString){
buildEngine(engineString);
}
private:
void buildEngine(std::string engineString){
cout << engineString;
}
}
class SuperCar: Car {
//...
SuperCar(): Car("roff"){
}
}
SuperCar sc; // Roff
Key Insight
- Don’t call virtual function during constructor or destructor as it will not achieve what you expect and can cause confusion and hard to debug.
Return reference to this for assignment operator
class Widget {
public:
Widget& operator=(const Widget& rhs){
// ...
return *this;
}
};
//...
Widget a, b, c;
// some assignment
a = b = c; // assign b to c and the assigned b to a
While this is not very commonly used, it is a convention followed by the std library and should be followed to avoid any inconsistency when working with the api of std library.
Key Insight
- By convention of other std library. Assignment are often chained and return
*thiswill help address any inconsistency.
Check for self when using assignment operator
When class contains pointer to a dynamically allocated item (new, malloc, etc), if we do not check for identity (i.e rhs is same as self), we could run into issues where we free the dynamically allocated fields which also freed the rhs object which resulted in inconsistency/unexpected states. To combat this, we do the following
Widget& Widget::operator=(const Widget& rhs){
if(this == &rhs) return *this;
auto ptr = new Object(*rhs.dynamicPtr);
delete dynamicPtr; //dynamically allocated pointer
dynamicPtr = ptr;
return *this;
}
Note we assign the value to a temporary variable first. The reason for this is because if initializing new object caused an exception, we would be in an inconsistency state again.
Key Insigts
- Make sure operator= is well-behaved when an object is assigned to itself. Techniques include comparing addresses of source and target objects, careful statement ordering, and copy-and-swap.
- Make sure that any function operating on more than one object behaves correctly if two or more of the objects are the same.
Copy Function Should Copy All the Attribute
When we define our con assignment operator (i.e operator=(...)) or copy operator (i.e Widget(const Widget& rhs)), the compiler will not raise any warning when we missed a field or copied incorrectly. This can often lead to bug where new field is updated and the constructor and copy function and not updated which lead to inconsistent states.
class Widget {
public:
Widgt(const Widget& rhs)
: size_(rhs.size_) {
// forgot to copy the direction,
// no warning are raised.
}
Widget& operator=(const Widget& rhs){
size_ = rhs.size_;
// forgot to copy the direction,
// no warning are raised.
}
private:
int size_;
int direction_; // recently added field
}
Derived Class Must Copy all the base class field
When a derived class is copied, the field under the sub class is not copied. The responsibility is on the derived class. The following might work in java but will often lead to error in cpp.
class FastWidget: public Widget {
public:
FastWidget(const FastWidget& rhs)
: speed_(rhs.speed_) {
// forgot to copy the, name and direction,
// no warning are raised.
}
Widget& operator=(const Widget& rhs){
speed_ = rhs.speed_;
// forgot to copy the, name and direction,
// no warning are raised.
return *this; // don't forget return type
}
private:
int speed_;
}
The correct approach here is to call the base class copy/assignment operator like so.
class FastWidget: public Widget {
public:
FastWidget(const FastWidget& rhs)
: Widget(rhs), speed_(rhs.speed_) {
// forgot to copy the, name and direction,
// no warning are raised.
}
Widget& operator=(const Widget& rhs){
Widget::operator=(rhs);
speed_ = rhs.speed_;
// forgot to copy the, name and direction,
// no warning are raised.
return *this; // don't forget return type
}
private:
int speed_;
}
Avoid duplicate code by using init
It doesn’t make sense for copy operator to call assignment operator or vice versa. However, there might be many derived logic that are similar among the two. To combat the duplication, it might be helpful to create a init function that assign the fields based on input and reuse across both operator and copy.
Key Insights
- Copying function should be made sure to copy all of the object data and the base class parts.
- Double check if there are custom copy/=operator for a class before adding a field
- Avoid duplication across copy and = operator by having init function that assign the fields.
Use objects to manage resources
Delete keyword inside object destructor
It is more preferential to have the life cycle events to initialize and close the resources managed within object constructor and destructor. The reason being that during the code execution, when exception or any unexpected change in execution location (goto, break, return, etc), the object destructor will be called to ensure that memory, db, connection, sockets, or any other resources are closed and leave no open loops that could cause many bad consequences.
Use of smart pointer to handle deletion new references
A commonly used object used in cpp to manage the life cycle of a newly initialized object in the heap is std::autoptr. The usage is as follow.
{
std::auto_ptr<Foo> pFoo(new Foo());
// .. do something with foo
pFoo->bar();
} // foo is automatically deallocated, i.e deleted
Note that the autoptr doesn’t support any copying which and rather handles copy and assignment operation as transfer ownership. Take the following example.
std::auto_ptr<Investment> pInv1(new Investment());
std::auto_ptr<Investment> pInv2(pInv1);
// pass ref from pInv1 to pInv2.
// pInv1 no longer have this object and point to null
pInv1 = pInv2;
// pass ref from pInv2 to pInv1.
// pInv2 no longer have this object and point to null
Another commonly used object is std::shared_ptr which allows the copy of pointer and will only delete the pointer once all the references to that object have been out of scope.
{
std::shared_ptr<Investment> pInv1(new Investment());
{
std::shared_ptr<Investment> pInv2(pInv1);
// do stuff with pInv2
}
{
std::shared_ptr<Investment> pInv3(pInv1);
// do stuff with pInv3
}
} // no reference to pInv1 exist anymore, deleted
Limitation of Smart Pointers
Unlike the garbage collector like java or C#, c++ shared pointer will not prevent cycles when it comes to shared pointers.
shared_ptr<ListNode> node1 = new ListNode();
shared_ptr<ListNode> node2 = new ListNode();
node1->setNext(node2);
node2->setNext(node1);
// this creates a cycle between two shared pointer and will never be reclaimed.
The smart pointer does not work with some other native array type such as the following
std::auto_ptr<std::string> aps(new std::string[10]);
// only delete will be used
std::tr1::shared_ptr<int> spi(new int[1024]);
// same problem
Key Take Away
- To prevent resource leaks, use RAII objects that acquire resources in their constructors and release them in their destructors.
- Two commonly useful RAII classes are
shared_ptrandauto_ptr.shared_ptris usually the better choice, because its behavior when copied is intuitive. Copying onauto_ptrsets it to null.
Define Copy Behavior In Resource Managing Classes
Prohibit Copying
In many cases, it makes no sense to allow RAII objects to be copied. This is likely to be true for a class like Lock, because it rarely makes sense to have “copies” of synchronization primitives. When copying makes no sense for an RAII class, you should prohibit it. Item 6 explains how to do that: declare the copying operations private. For Lock, that could look like this.
clss Lock: private Uncopyable {
public:
// ...
}
See [[#Block Generated Function]] for detail implementation of Uncopyable.
Reference Counting
Sometimes it’s desirable to hold on to a resource until the last object using it has been destroyed. When that’s the case, copying an RAII object should increment the count of the number of objects referring to the resource. This is the meaning of “copy” used by std::shared_ptr.
One way to achieve this is through the use of the shared_ptr by using the second argument of to specify the deleter function. Here is an example.
class Lock {
public:
void unlock(Mutex* pm) {
// unlock logic here
// ...
}
explicit Lock(Mutex *pm): mutexPtr(pm, unlock) {
lock(mutexPtr.get());
// see Item 15 for info on "get"
}
private:
std::shared_ptr<Mutex> mutexPtr;
}
In the above example, we see the unlock function being passed as second argument when initializing the shared ptr, the function will be called instead of the default delete keyword when the reference reaches zero.
Copy The Underlying Resources
Sometimes you can have as many copies of a resource as you like, and the only reason you need a resource-managing class is to make sure that each copy is released when you’re done with it. In that case, copying the resource-managing object should also copy the resource it wraps. That is, copying a resource-managing object performs a “deep copy.” Some implementations of the standard string type consist of pointers to heap memory, where the characters making up the string are stored. Objects of such strings contain a pointer to the heap memory. When a string object is copied, a copy is made of both the pointer and the memory it points to. Such strings exhibit deep copying.
Transfer Ownership
On rare occasion, you may wish to make sure that only one RAII object refers to a raw resource and that when the RAII object is copied, ownership of the resource is transferred from the copied object to the copying object. This is the meaning of “copy” used by auto_ptr.
Key Insights
- Copying an RAII object entails copying the resource it manages, so the copying behavior of the resource determines the copying behavior of the RAII object.
- Common RAII class copying behaviors are disallowing copying and performing reference counting, but other behaviors are possible.
Define Raw Access-or For Resources Manage Classes
In many situation, the external API might often work with the underlying resource class which means that the resource manage class we use to protect the resource leak should either provide underlying API inside the resource class implementation or provide the access to the raw resources when needed. While it is safer to provide the internal API, it isn’t always practical as there could be large number of dependency that are only required in certain use cases but isn’t always required. In most case, it is most probable to have raw access for the sake of simplicity.
Offer Explicit Get Function
In most situation, the prefer way is to provide a explicit getter which fetches the underlying resources reference. In the std::shared_ptr and std::auto_ptr we have the get() interface which returns the reference of the type within their template. For example,
// api
int getTotalCost(const Investment* pi);
class Investment {
// ...
}
std::shared_ptr<Investment> pInv(createInvestment());
getTotalCost(pInv.get()); // call the
In some situation, it might also be preferable to have deference in order to call the internal function. We could use the arrow function for std::shared_ptr and * for std::auto_ptr. For example
class Investment {
public:
bool isTaxFree() const;
}
std::shared_ptr<Investment> pInv(createInvestment());
pInv->isTaxFree();
std::auto_ptr<Investment> autoInv(createInvestment());
(*autoInv).isTaxFree();
Implicit Casting To Resource Type
Sometimes, if the underlying resources is constantly being used and the constant use of .get() API is causing developer inconvenience and causing developer to work around that by not using the manage class. In these cases, it might be possible to add a implicit conversion operator to support the functionality. Suppose we have the following.
void changeFontSize(FontHandle f, int newSize);
class Font {
public:
operator FontHandle() const { return f; }
private:
FontHandle f; // the underlying resources
}
Font font(getFontHandle());
int newSize = 20;
changeFontSize(font, newSize);
// font will get cast to font handle, all is good
This makes the usage of the API easy and strait forward. However, it is still often not preferred because the implicit casting can often lead to unexpected behavior. Especially when the developer is unfamiliar with the code base and some of these more nuanced use cases. One such example would be.
FontHandle f2;
{
Font f1(getFontHandle());
f2 = f1;
// was copy to get another Font or
// increase reference counting
} // f1 is out of scope and destroyed
f2.doStuff() // err f1 is already destroyed.
In the above case, since the cast to raw resource was unexpected, the deletion of the object when out of scope is also unexpected. As rule of thumb, try to not have too many implicit casting for resource management classes.
Key Insights
- APIs often require access to raw resources, so each RAII class should offer a way to get at the resource it manages.
- Access may be via explicit conversion or implicit conversion. In general, explicit conversion is safer, but implicit conversion is more convenient for clients.
Corresponding of new and delete
C++ offers ways to initialize arrays using the new syntax. For example.
std::string * stringArr = new std::string[100];
// ...
delete stringArr;
However, simply running the delete on the pointer is not sufficient as delete will treat the variable as a single object and only delete the first instance of the array, leaving 99 more instances not properly destroyed. To properly destroy array based initialized object. We can do the following.
std::string * stringArr2 = new std::string[100];
delete [] stringArr2;
In this implementation, we properly free the entire array of object. Note that improper use of [] on singular object will cause undefined behavior as it will treat the content of the object as part of the array count and possibly free unintended memory.
std:string* str = new std::string;
delete [] str; // undefined behavior
To avoid these problematic usage, try to resort to the more standard collection such as std::vector to properly free the item and make the maintaining the code easier.
Key Insights
- If you use
[]in a new expression, you must use[]in the corresponding delete expression. If you don’t use[]in a new expression, you mustn’t use[]in the corresponding delete expression.
Declare Share Pointer And New in single statement
Key Take Away
- Store new objects in smart pointers in standalone statements. Failure to do this can lead to subtle resource leaks when exceptions are thrown.
Item 18 - Make interface easy to use correctly but hard to use incorrectly
Example 1 - Date as input
struct Day {
explicit Day (int d):val(d){}
int val;
}
struct Month {
explicit Month (int m):val(m){}
int val;
}
struct Year {
explicit Year (int y):val(y){}
int val;
}
struct Date {
explicit Date (Year y, Month m, Day d){
...
}
}
Date d(30, 3, 1995); // wrong types
Date d2(Day(30), Month(3), Year(1995)); // wrong order
Date d3(Year(1995), Month(3), Day(30)); // correct
Key Take Away
- Good interfaces are easy to use correctly and hard to use incorrectly. Your should strive for these characteristics in all your interfaces.
- Ways to facilitate correct use include consistency in interfaces and behavioral compatibility with built-in types.
- Ways to prevent errors include creating new types, restricting operations on types, constraining object values, and eliminating client resource management responsibilities.
- TR1::shared_ptr supports custom deleters. This prevents the cross-DLL problem, can be used to automatically unlock mutexes (see Item 14), etc.
Item 19: Treat class design as type design
Questions to ask when thinking about a new class
- What kind of type conversions are allowed for your new type?
- Your type exists in a sea of other types, so should there be conversions between your type and other types? If you wish to allow objects of type T1 to be implicitly converted into objects of type T2, you will want to write either a type conversion function in class T1 (e.g., operator T2) or a non-explicit constructor in class T2 that can be called with a single argument. If you wish to allow explicit conversions only, you’ll want to write functions to perform the conversions, but you’ll need to avoid making them type conversion operators or non-explicit constructors that can be called with one argument. (For an example of both implicit and explicit conversion functions, see Item 15)
- What operators and functions make sense for the new type?
- The answer to this question determines which functions you’ll declare for your class. Some functions will be member functions, but some will not (see Items 23, Item 24 and Item 46).
- What standard functions should be disallowed?
- Those are the ones you’ll need to declare private (see Item 6).
- Who should have access to the members of your new type?
- This question helps you determine which members are public, which are protected, and which are private. It also helps you determine which classes and/or functions should be friends, as well as whether it makes sense to nest one class inside another.
- What is the “undeclared interface” of your new type?
- What kind of guarantees does it offer with respect to performance, exception safety (see Item 29), and resource usage (e.g., locks and dynamic memory)? The guarantees you offer in these areas will impose constraints on your class implementation.
- How general is your new type?
- Perhaps you’re not really defining a new type. Perhaps you’re defining a whole family of types. If so, you don’t want to define a new class, you want to define a new class template.
- Is a new type really what you need?
- If you’re defining a new derived class only so you can add functionality to an existing class, perhaps you’d better achieve your goals by simply defining one or more non-member functions or templates.
Key Inisght
- Class design is type design. Before defining a new type, be sure to consider all the issues discussed in this Item.
Item 27
- Avoid cast when possible. If needed use the 4 dedicated cast instead of the native C cast to improve on ease of code search and compiler understanding.
static_castused to cast object type like (int to float).const_castcast away the const-ness of the typedynamic_castchange from parent type (base) to children class (derived). Very expensive when nesting type. Each layer is one strcmp.reinterpret_castlow level cast that is used for portability and platform specific purpose. Very rarely used.