To develop an interface that is "easy to be used correctly, not easy to be misused", we must first consider what mistakes users may make. Suppose you design a constructor for a class that represents a date:
class Data{ public: Date(int month, int day, int year); ... };
At first glance, the interface is reasonable. But its users can easily make at least two mistakes. First, they may pass parameters in the wrong order:
Date d(30, 3, 1995);
Secondly, they may pass on an invalid month or day:
Date d(2, 30, 1995);
Many client errors can be prevented by importing new types. In this case, let's import simple override types to distinguish days, months, and years, and then use them in the Date constructor:
struct Day{ explicit Day(int d) :val(d) { } int val; }; struct Month{ explict Month(int m) : val(m) { } int val; }; struct Year{ explict Year(int y) :val(y) { } int val; }; class Date{ public: Date(const Month& m, const Day& d, const Year& y); ... }; Date d(30, 3, 1995); //Wrong! Incorrect type Date d(Day(30), Month(3), Year(1995)); //Wrong! Incorrect type Date d(Month(3), Day(30), Year(1995)); //OK, the type is correct
Make Day, Mouth and Year mature and well-trained classes and encapsulate their data, better than simply using the structs mentioned above. But even structs have demonstrated enough: smart and careful introduction of new types can be magical in preventing "interface misuse".
Once the correct type is located, it is sometimes reasonable to limit its value. For example, there are only 12 valid months in a year, so Mouth should reflect this fact. One way is to use enum to represent months, but enum does not have the type security we want, such as enums can be used as an ints. A safer solution is to predefine all valid Months:
class Month{ public: static Month Jan() { return Mouth(1); } //Function to return the valid month static Month Feb() { return Month(2); } ... static Month Dec() { return Month(12); } ... private: explict Month(int m); //Prevent the generation of new months, which are month-specific data ... }; Date d(Month::Mar(), Day(30), Year(1995) );
tr1::shared_ptr provides a constructor that accepts two arguments: a managed pointer and a deletor called when the number of references becomes zero. This inspires us to create a null tr1::shared_ptr and use getRidOfInvestment as its deletor, like this:
std::tr1::shared_ptr<Investment> pInv(0, getRidOfInvestment); //Attempt to create a null shaerd_ptr //And carry a custom deletor //This formula cannot be compiled
tr1::shared_ptr constructor whose first parameter must be a pointer, and 0 is not a pointer, but an int. Yes, it can be converted into a pointer, but it's not good enough in this case, because tr1::shared_ptr insists on an absolute pointer. Transition (cast) can solve this problem:
std::tr1::shared_ptr<Investment> pInv(static_casst<Investment*> (0), getRidOfInvestment);//Establish a null shaerd_ptr and //getRidOfInvesment is a deletor
Therefore, if we want to implement createInvestment to return a tr1::shared_ptr with getRidOfInvestment function as a deletor, the code looks like this:
std::tr1::shared_ptr<Investment> createInvesment(){ std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*> (0)),getRidOfInvestment); retVal = ...; //Make retVal point to the correct object return retVal; }
Conclusion:
Good interfaces are easy to use correctly and not easy to be misused. You should strive to achieve these qualities in all your interfaces.
"Promoting proper use" approaches include interface consistency and compatibility with built-in types of behavior.
"Preventing misuse" includes establishing new types, restricting operations on types, binding object values, and eliminating customer resource management responsibilities.
tr1::shared_ptr supports custom personality deletors. This can prevent DLL problems, but not be used to automatically unlock mutexes and so on.